shithub: spread

Download patch

ref: b16375893ad58a69c9797fb76c97631de0513eb0
author: sirjofri <[email protected]>
date: Wed Jul 3 11:56:10 EDT 2024

adds files

diff: cannot open b/test//null: file does not exist: 'b/test//null'
--- /dev/null
+++ b/README
@@ -1,0 +1,79 @@
+Spreadsheet Editor
+
+This program builds around hoc(1).
+
+
+# Spreadsheet files
+
+They are divided into two blocks, separated by a line containing three `%`:
+
+1. hoc script. Loaded verbatim to hoc. Use this to add functionality.
+2. cells and cell contents.
+
+A sample file could look like this:
+
+	func t(a) {
+		print a
+	}
+	%%%
+	A1=3
+	A2;hello
+	A3=5
+	A4=A1()+A3()
+
+The general syntax of cells should be quite obvious, but it's worth noting
+that cells divided by an `=` sign will end up being calculation functions,
+while cells divided by a `;` sign are string literals. Both will end up
+being hoc functions.
+
+# Usage
+
+	spread [-di] file
+
+- `file` will be loaded as a spreadsheet
+- `-d` enables extra debug output
+- `-i` opens spread in CLI mode (see below)
+
+# Modes
+
+## GUI mode
+
+**TODO**
+
+A few notes:
+
+- Cell syntax will be the same as in the files
+- Function/math cells will start with `=` for editing (output
+  is function result)
+- String cells will have no prefix; input and output are the same
+
+## CLI mode
+
+For now, this opens a direct channel to hoc. All commands entered will
+be forwarded to the hoc process unchanged.
+
+Example session for previous example program:
+
+	A4()
+	→ 8
+	A1()
+	→ 3
+	A1()*A3()
+	→ 15
+	A1()+2*A3()
+	→ 13
+
+# Open questions
+
+## Hoc limitations
+
+- Range support. Since hoc doesn't support something like `eval`,
+  it is impossible to support ranges (`A3()..A5()`) out of the box.
+  If we need ranges we need to find a good solution or at least a
+  solid workaround, like building a list dynamically.
+
+## Bugs
+
+Sure, there are many. Known issues are:
+
+- Cyclic dependencies are not allowed, but also not really checked for!
--- /dev/null
+++ b/cells.c
@@ -1,0 +1,290 @@
+#include <u.h>
+#include <libc.h>
+#include <regexp.h>
+#include "spread.h"
+
+#define GROW 64
+#define PGROW 4
+
+typedef struct Cellblock Cellblock;
+struct Cellblock {
+	Cell *cells;
+	int size;
+	int num;
+};
+
+Cellblock block;
+
+Reprog *funcregexp;
+
+void
+addcell(P cell, char *value, int type)
+{
+	Cell *c;
+	
+	assert(type != FUNCTION || type != STRING);
+	assert(value);
+	
+	if (!block.cells) {
+		block.size = GROW;
+		block.cells = mallocz(sizeof(Cell) * block.size, 1);
+		assert(block.cells);
+		block.num = 0;
+	}
+	
+	if (c = getcell(cell)) {
+		if (c->value)
+			free(c->value);
+		c->value = strdup(value);
+		c->type = type;
+		return;
+	}
+	
+	if (block.num + 1 >= block.size) {
+		block.size += GROW;
+		block.cells = realloc(block.cells, sizeof(Cell) * block.size);
+		assert(block.cells);
+		memset(&block.cells[block.num], 0, block.size - block.num);
+	}
+	c = &block.cells[block.num++];
+	c->p = cell;
+	c->value = strdup(value);
+	c->type = type;
+}
+
+void
+rmcell(P cell)
+{
+	int i;
+	Cell *c;
+	
+	for (i = 0; i < block.num; i++) {
+		c = &block.cells[i];
+		
+		if (PEQ(c->p, cell))
+			continue;
+		
+		if (c->value)
+			free(c->value);
+		c->value = nil;
+		c->type = 0;
+		c->p.x = 0;
+		c->p.y = 0;
+	}
+}
+
+Cell*
+getcell(P cell)
+{
+	int i;
+	Cell *c;
+	
+	for (i = 0; i < block.num; i++) {
+		c = &block.cells[i];
+		if (c->type != FUNCTION && c->type != STRING)
+			continue;
+		if (PEQ(cell, c->p))
+			return c;
+	}
+	return nil;
+}
+
+void
+foreachcell(void (*f)(Cell*))
+{
+	int i;
+	Cell *c;
+	
+	for (i = 0; i < block.num; i++) {
+		c = &block.cells[i];
+		if (c->type != FUNCTION && c->type != STRING)
+			continue;
+		f(c);
+	}
+}
+
+static char*
+findvaluep(char *s, P *p)
+{
+	Resub match;
+	
+	memset(&match, 0, sizeof(Resub));
+	if (regexec(funcregexp, s, &match, 1)) {
+		*p = atop(match.sp);
+		return match.ep;
+	}
+	return nil;
+}
+
+static int
+hascell(Cell *c, Cell *d)
+{
+	int i;
+	if (!c->points)
+		return 0;
+	
+	for (i = 0; i < c->num; i++)
+		if (c->points[i] == d)
+			return 1;
+	return 0;
+}
+
+static void
+addcelldep(Cell *c, Cell *d)
+{
+	if (hascell(c, d))
+		return;
+	
+	if (!c->points) {
+		c->size = PGROW;
+		c->points = mallocz(c->size * sizeof(Cell*), 1);
+		assert(c->points);
+		c->num = 0;
+	}
+	if (c->num + 1 >= c->size) {
+		c->size += PGROW;
+		c->points = realloc(c->points, c->size * sizeof(Cell*));
+		assert(c->points);
+		memset(&c->points[c->num], 0, c->size - c->num);
+	}
+	c->points[c->num] = d;
+	c->num++;
+}
+
+static void
+celldeps(Cell* c)
+{
+	Cell *d;
+	char *s;
+	P p;
+	
+	if (c->type != FUNCTION)
+		return;
+	
+	c->indeg = 0;
+	s = c->value;
+	while (s = findvaluep(s, &p)) {
+		c->indeg++;
+		d = getcell(p);
+		if (!d) {
+			dumpcells();
+			sysfatal("cannot find cell %s (%d %d)", ptoa(p), p.x, p.y);
+		}
+		addcelldep(d, c);
+	}
+}
+
+static Cell*
+findindegzero(void)
+{
+	Cell *c;
+	for (int i = 0; i < block.num; i++) {
+		c = &block.cells[i];
+		if (c->type != FUNCTION && c->type != STRING)
+			continue;
+		if (c->indeg == 0)
+			return c;
+	}
+	return nil;
+}
+
+static void
+cleandeps(Cell *c)
+{
+	for (int i = 0; i < c->num; i++) {
+		c->points[i]->indeg--;
+	}
+	free(c->points);
+	c->points = nil;
+	c->size = 0;
+	c->num = 0;
+}
+
+void
+sortcells(void)
+{
+	Cell *c;
+	Cellblock b;
+	
+	// build DAG information
+	funcregexp = regcomp("[A-Z]+[0-9]+[(]+[)]+");
+	foreachcell(celldeps);
+	free(funcregexp);
+	funcregexp = nil;
+	
+	b.size = block.size;
+	b.num = 0;
+	b.cells = mallocz(sizeof(Cell) * b.size, 1);
+	assert(b.cells);
+	
+	// sort DAG to linear list
+	while (c = findindegzero()) {
+		cleandeps(c);
+		memcpy(&b.cells[b.num], c, sizeof(Cell));
+		memset(c, 0, sizeof(Cell));
+		b.num++;
+	}
+	
+	block.num = b.num;
+	block.cells = b.cells;
+	memset(&b, 0, sizeof(Cellblock));
+}
+
+void
+gccells()
+{
+	
+}
+
+static void
+dumpcellpoints(Cell *c)
+{
+	Cell **n;
+	if (!c->points) {
+		fprint(2, "%5s (%d) : <none>\n", ptoa(c->p), c->indeg);
+		return;
+	}
+	fprint(2, "%5s (%d) :", ptoa(c->p), c->indeg);
+	for (n = c->points; *n; n++) {
+		fprint(2, " %s", ptoa((*n)->p));
+	}
+	fprint(2, "\n");
+}
+
+void
+dumpcells(void)
+{
+	int i;
+	Cell *c;
+	
+	fprint(2, "dump cells:\n");
+	if (!block.cells) {
+		fprint(2, "empty\n");
+		exits(nil);
+	}
+	
+	for (i = 0; i < block.size; i++) {
+		c = &block.cells[i];
+		switch (c->type) {
+		case 0:
+			fprint(2, "%5d : invalid\n", i);
+			break;
+		case FUNCTION:
+			fprint(2, "%5d : %s = %s\n", i, ptoa(c->p), c->value);
+			break;
+		case STRING:
+			fprint(2, "%5d : %s : %s\n", i, ptoa(c->p), c->value);
+			break;
+		default:
+			fprint(2, "%5d : error\n", i);
+		}
+	}
+	
+	fprint(2, "Cell points (DAG):\n");
+	for (i = 0; i < block.size; i++) {
+		c = &block.cells[i];
+		if (c->type == 0)
+			continue;
+		dumpcellpoints(c);
+	}
+}
--- /dev/null
+++ b/engine.c
@@ -1,0 +1,372 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "spread.h"
+
+char Einitengine[] = "initengine: %r";
+
+int p[3];
+
+typedef struct Strchan Strchan;
+struct Strchan {
+	/* data */
+	QLock l;
+	Rendez full;
+	Rendez empty;
+	char *s;
+	Response r;
+	
+	/* want */
+	QLock w;
+	int want;
+};
+
+Strchan*
+mkchan(void)
+{
+	Strchan *c;
+	c = mallocz(sizeof(Strchan), 1);
+	c->full.l = &c->l;
+	c->empty.l = &c->l;
+	return c;
+}
+
+void
+request(Strchan *c, int want)
+{
+	qlock(&c->w);
+	c->want = want;
+	qunlock(&c->w);
+}
+
+void
+send(Strchan *c, char *s, int error)
+{
+	qlock(&c->w);
+	if (!c->want) {
+		qunlock(&c->w);
+		return;
+	}
+	c->want = 0;
+	qunlock(&c->w);
+	qlock(&c->l);
+	while (c->r.msg) {
+		rsleep(&c->full);
+	}
+	c->r.msg = strdup(s);
+	c->r.error = error;
+	rwakeup(&c->empty);
+	qunlock(&c->l);
+}
+
+Response
+recv(Strchan *c)
+{
+	Response r;
+	
+	qlock(&c->l);
+	while (!c->r.msg) {
+		rsleep(&c->empty);
+	}
+	r = c->r;
+	c->r.msg = nil;
+	c->r.error = 0;
+	rwakeup(&c->full);
+	qunlock(&c->l);
+	return r;
+}
+
+Strchan *outchan;
+
+enum {
+	L_INVALID = 0,
+	L_FUNC = FUNCTION,
+	L_STRING = STRING,
+};
+
+int
+spawnerrrdr(void)
+{
+	Biobuf *bin;
+	char *s;
+	
+	switch (rfork(RFPROC|RFMEM|RFFDG)) {
+	case -1:
+		sysfatal(Einitengine);
+	case 0: /* child */
+		break;
+	default: /* parent */
+		return 1;
+	}
+	
+	bin = Bfdopen(p[2], OREAD);
+	if (!bin)
+		sysfatal(Einitengine);
+	
+	while (s = Brdstr(bin, '\n', 1)) {
+		if (debug)
+			fprint(2, "←hoc: × %s\n", s);
+		send(outchan, s, 1);
+		free(s);
+	}
+	Bterm(bin);
+	exits(nil);
+}
+
+int
+inithoc(void)
+{
+	int in[2];
+	int out[2];
+	int err[2];
+	Biobuf *bin;
+	char *s;
+	
+	if (pipe(in) < 0)
+		sysfatal(Einitengine);
+	if (pipe(out) < 0)
+		sysfatal(Einitengine);
+	if (pipe(err) < 0)
+		sysfatal(Einitengine);
+	
+	switch (fork()) {
+	case -1: /* error */
+		sysfatal(Einitengine);
+		break;
+	case 0: /* child hoc */
+		dup(out[1], 1);
+		dup(in[0], 0);
+		dup(err[1], 2);
+		close(out[1]);
+		close(out[0]);
+		close(in[0]);
+		close(in[1]);
+		close(err[1]);
+		close(err[0]);
+		execl("/bin/hoc", "hoc", nil);
+		sysfatal(Einitengine);
+		break;
+	default: /* parent */
+		close(out[1]);
+		close(in[0]);
+		close(out[1]);
+		p[0] = in[1];
+		p[1] = out[0];
+		p[2] = err[0];
+		break;
+	}
+	
+	outchan = mkchan();
+	
+	switch (rfork(RFPROC|RFMEM|RFFDG)) {
+	case -1: /* error */
+		sysfatal(Einitengine);
+	case 0: /* child reader */
+		break;
+	default: /* parent */
+		return spawnerrrdr();
+	}
+	
+	bin = Bfdopen(p[1], OREAD);
+	if (!bin)
+		sysfatal(Einitengine);
+	
+	while (s = Brdstr(bin, '\n', 1)) {
+		if (debug)
+			fprint(2, "←hoc: ← %s\n", s);
+		send(outchan, s, 0);
+		free(s);
+	}
+	Bterm(bin);
+	exits(nil);
+}
+
+static void
+hocwrite(char *s, Response *o)
+{
+	request(outchan, !!o);
+	fprint(p[0], "%s", s);
+	
+	if (debug)
+		fprint(2, "→hoc: %s", s);
+	
+	if (o) {
+		*o = recv(outchan);
+	}
+}
+
+static int
+linetype(char *s)
+{
+	char *c;
+	int xalpha, xnum, falpha, fnum, found;
+	
+	xalpha = 1;
+	xnum = 0;
+	falpha = 0;
+	fnum = 0;
+	found = L_INVALID;
+	while ((c = s++) && *c) {
+		if (*c != 0 && found)
+			return found;
+		switch (*c) {
+		case 0: /* end of line */
+			return L_INVALID;
+		case ';': /* string line */
+			if (!falpha || !fnum)
+				return L_INVALID;
+			found = L_STRING;
+			continue;
+		case '=': /* hoc function line */
+			if (!falpha || !fnum)
+				return L_INVALID;
+			found = L_FUNC;
+			continue;
+		}
+		if (*c >= '0' && *c <= '9') {
+			if (!xnum || !falpha)
+				return L_INVALID;
+			fnum = 1;
+			xalpha = L_INVALID;
+		}
+		if ((*c >= 'a' && *c <= 'z')
+		 || (*c >= 'A' && *c <= 'Z') ) {
+			if (!xalpha)
+				return L_INVALID;
+			falpha = 1;
+			xnum = 1;
+		}
+	}
+	return found;
+}
+
+void
+interactivehoc()
+{
+	Response o;
+	char *s;
+	Biobuf *bin;
+	
+	bin = Bfdopen(0, OREAD);
+	
+	while (s = Brdstr(bin, '\n', 0)) {
+		if (s[0] == '\n')
+			continue;
+		hocwrite(s, &o);
+		free(s);
+		if (o.msg)
+			print("%s %s\n", o.error ? "×" : "→", o.msg);
+	}
+}
+
+char Hfunc[] = "func %s() { return %s }\n";
+char Hstring[] = "func %s() { print \"%s\" }\n";
+
+static void
+sendctohoc(Cell *c)
+{
+	char *h;
+	char *buf;
+	
+	switch (c->type) {
+	case FUNCTION:
+		h = Hfunc;
+		break;
+	case STRING:
+		h = Hstring;
+		break;
+	default:
+		sysfatal("code error");
+	}
+	
+	buf = smprint(h, ptoa(c->p), c->value);
+	hocwrite(buf, nil);
+	free(buf);
+}
+
+static void
+eaddcell(char *s, int type)
+{
+	P p;
+	char *a;
+	
+	switch (type) {
+	case L_FUNC:
+		a = strchr(s, '=');
+		break;
+	case L_STRING:
+		a = strchr(s, ';');
+		break;
+	default:
+		sysfatal("bad type: %d not in [1,2]", type);
+	}
+	
+	*a = 0;
+	a++;
+	
+	p = atop(s);
+	addcell(p, a, type);
+}
+
+int
+loadfile(char *file)
+{
+	Biobuf *bin;
+	char *s;
+	int type;
+	
+	bin = Bopen(file, OREAD);
+	if (!bin) {
+		werrstr("open: %r");
+		return 0;
+	}
+	
+	while (s = Brdstr(bin, '\n', 0)) {
+		if (strcmp("%%%\n", s) == 0) {
+			free(s);
+			break;
+		}
+		
+		hocwrite(s, nil);
+		free(s);
+	}
+	
+	while (s = Brdstr(bin, '\n', 1)) {
+		type = linetype(s);
+		switch(type) {
+		case L_INVALID:
+			if (debug) {
+				fprint(2, "!hoc: %s\n", s);
+			}
+			break;
+		case L_FUNC:
+		case L_STRING:
+			eaddcell(s, type);
+			break;
+		}
+		free(s);
+	}
+	
+	Bterm(bin);
+	
+	sortcells();
+	
+	foreachcell(sendctohoc);
+	
+	return 1;
+}
+
+Response
+getvalue(char *cell)
+{
+	char *s;
+	Response o;
+	
+	toupperil(cell);
+	
+	o.msg = nil;
+	s = smprint("%s()\n", cell);
+	hocwrite(s, &o);
+	free(s);
+	return o;
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,10 @@
+</$objtype/mkfile
+
+TARG=spread
+OFILES=\
+	spread.$O\
+	engine.$O\
+	util.$O\
+	cells.$O\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/spread.c
@@ -1,0 +1,55 @@
+#include <u.h>
+#include <libc.h>
+#include "spread.h"
+
+int debug = 0;
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-di] file\n", argv0);
+	exits("usage");
+}
+
+char *file = nil;
+int interactive = 0;
+
+void
+main(int argc, char **argv)
+{
+	ARGBEGIN{
+	case 'd':
+		debug++;
+		break;
+	case 'i':
+		interactive++;
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+	
+	if (argc != 1)
+		usage();
+	
+	fprint(2, "pid=%d\n", getpid());
+	
+	file = *argv;
+	
+	if (!inithoc())
+		sysfatal("%r");
+	
+	if (!loadfile(file))
+		sysfatal("%r");
+	
+//	dumpcells();
+	
+	if (interactive) {
+		interactivehoc();
+		exits(nil);
+	}
+	
+	Response r = getvalue("a4");
+	
+	fprint(2, "value: A4='%s' (error=%d)\n", r.msg, r.error);
+}
--- /dev/null
+++ b/spread.h
@@ -1,0 +1,50 @@
+extern int debug;
+
+typedef struct P P;
+typedef struct Node Node;
+typedef struct Cell Cell;
+typedef struct Response Response;
+
+#define FUNCTION 1
+#define STRING 2
+
+int inithoc(void);
+void interactivehoc(void);
+int loadfile(char *file);
+int writefile(char *file);
+Response getvalue(char *cell);
+
+void addcell(P cell, char *value, int type);
+void rmcell(P cell);
+Cell* getcell(P cell);
+void gccells(void);
+void dumpcells(void);
+void foreachcell(void (*f)(Cell*));
+void sortcells(void);
+
+void toupperil(char*);
+P atop(char*);
+char* ptoa(P); // resulting char* is mallocd
+
+#define PEQ(a, b) ((a).x == (b).x && (a).y == (b).y)
+
+struct P {
+	int x;
+	int y;
+};
+
+struct Cell {
+	P p;
+	char *value;
+	int type;
+	
+	Cell **points;
+	int size;
+	int num;
+	int indeg;
+};
+
+struct Response {
+	char *msg;
+	int error;
+};
--- /dev/null
+++ b/test/test.rc
@@ -1,0 +1,19 @@
+#!/bin/rc
+
+rfork en
+ramfs
+
+cat <<EOF >/tmp/test.sp
+func t(a) {
+	print a
+}
+%%%
+A1=3
+A2;hello
+A4=A1()+A3()
+A3=5
+EOF
+
+../6.out -d /tmp/test.sp
+prompt=('; ' '	')
+rc -i
--- /dev/null
+++ b/util.c
@@ -1,0 +1,71 @@
+#include <u.h>
+#include <libc.h>
+#include "spread.h"
+
+void
+toupperil(char *s)
+{
+	while (*s) {
+		if (*s >= 'a' && *s <= 'z')
+			*s = (*s)-'a'+'A';
+		s++;
+	}
+}
+
+int
+aton(char *a, char **b)
+{
+	int ret = 0;
+	
+	while (*a && *a >= 'A' && *a <= 'Z') {
+		ret *= 'Z' - 'A' + 1;
+		ret += *a - 'A' + 1;
+		a++;
+	}
+	if (b)
+		*b = a;
+	return ret;
+}
+
+P
+atop(char *a)
+{
+	P p;
+	char *n;
+	
+	toupperil(a);
+	p.x = aton(a, &n);
+	p.y = atoi(n);
+	return p;
+}
+
+char*
+ptoa(P p)
+{
+	char ab[10];
+	char buf[25];
+	int r;
+	int m = 'Z' - 'A' + 1;
+	char *a = ab;
+	char *b;
+	
+	do {
+		r = p.x%m;
+		*a = r + 'A' - 1;
+		a++;
+		p.x /= m;
+	} while (p.x > 0 && a < (ab + sizeof(ab)));
+	*a = 0;
+	
+	b = buf;
+	a--;
+	while (a >= ab) {
+		*b = *a;
+		a--;
+		b++;
+	}
+	*b = 0;
+	
+	snprint(b, sizeof(buf) - (buf-b), "%d", p.y);
+	return strdup(buf);
+}