ref: b16375893ad58a69c9797fb76c97631de0513eb0
author: sirjofri <sirjofri@sirjofri.de>
date: Wed Jul 3 11:56:10 EDT 2024
adds files
--- /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);
+}