diff options
Diffstat (limited to 'prototypes')
| -rw-r--r-- | prototypes/connections/module.c | 156 | ||||
| -rw-r--r-- | prototypes/connections/module.h | 26 | ||||
| -rw-r--r-- | prototypes/connections/pure-run.c | 52 | ||||
| -rw-r--r-- | prototypes/connections/test-module.c | 260 |
4 files changed, 395 insertions, 99 deletions
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c index 2e5375c..4b5d15d 100644 --- a/prototypes/connections/module.c +++ b/prototypes/connections/module.c @@ -12,9 +12,9 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7 }; enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA }; /* Where an opcode may appear. */ -enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8, IN_TEXT = 16 }; +enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8, IN_TEXT = 16, IN_ROW = 32 }; enum { READ = IN_VIEW | IN_HANDLER }; -static const unsigned char magic[8] = {'P','U','R','E','M','O','D','4'}; +static const unsigned char magic[8] = {'P','U','R','E','M','O','D','5'}; /* Operand types popped (bottom to top), the type pushed, and where allowed. */ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP_COUNT] = { @@ -39,8 +39,8 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP [PURE_OP_MAX] = {2, {T_NUM, T_NUM}, T_NUM, READ}, [PURE_OP_CLAMP] = {3, {T_NUM, T_NUM, T_NUM}, T_NUM, READ}, [PURE_OP_STORE] = {1, {T_NUM}, 0, IN_HANDLER}, - [PURE_OP_JZ] = {1, {T_NUM}, 0, IN_HANDLER}, - [PURE_OP_JMP] = {0, {0}, 0, IN_HANDLER}, + [PURE_OP_JZ] = {1, {T_NUM}, 0, READ}, + [PURE_OP_JMP] = {0, {0}, 0, READ}, [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE}, [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED}, [PURE_OP_DOC_SIZE] = {0, {0}, T_NUM, IN_KEYED}, @@ -81,6 +81,7 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP [PURE_OP_INDEX_FIRST] = {0, {0}, T_INDEX, READ}, [PURE_OP_INDEX_LAST] = {0, {0}, T_INDEX, READ}, [PURE_OP_INDEX_NUM] = {1, {T_INDEX}, T_NUM, READ}, + [PURE_OP_ROW] = {0, {0}, T_INDEX, IN_ROW}, }; static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; } @@ -151,7 +152,7 @@ static bool uses_arg(uint8_t op) { case PURE_OP_TEXT_LINE: case PURE_OP_TEXT_SET: case PURE_OP_CUR_COUNT: case PURE_OP_LIST_COUNT: case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: case PURE_OP_ITEM_GET: case PURE_OP_ITEM_SET: case PURE_OP_INDEX_LOAD: case PURE_OP_INDEX_STORE: case PURE_OP_INDEX_FIRST: - case PURE_OP_INDEX_LAST: + case PURE_OP_INDEX_LAST: case PURE_OP_ROW: return true; default: return false; } @@ -170,35 +171,62 @@ static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t fie return m->record_field_text[m->record[m->state_ref[list]].first + field] ? T_TEXT : T_NUM; } +/* The stack a branch brings to its target, while the target is ahead. */ +typedef struct { uint32_t target; int height; unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK]; } Join; + +static bool same_stack(const Join *join, int height, const unsigned char *types, const unsigned char *owners) { + return join->height == height && !memcmp(join->types, types, (size_t)height) && + !memcmp(join->owners, owners, (size_t)height); +} + /* Types and heights are checked per instruction; a cursor's type records * which text it belongs to and an index's type which list. Branches go - * forward within the block and every join happens with an empty operand - * stack, so control flow never merges typed values. Handlers end empty, - * views with one value of their type. */ -static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_t event, + * forward within the block. Every path into a join brings the same stack + * (types and owners), so a value may cross a join, as in a conditional + * expression; at most 8 joins with a non-empty stack are ahead at once. + * Handlers end empty, views with one value of their type. A column (row + * is its list) may read the row it is evaluated for. */ +static bool verify_block(const PureModule *m, PureBlock block, int view, int row, uint32_t event, char *error, size_t size, unsigned index) { if (block.start > m->code_count || block.length > m->code_count - block.start) return reject(error, size, "code range outside module", index); if (view >= 0 && !block.length) return reject(error, size, "empty view", index); int input = view >= 0 ? -1 : event_input(event); const PureInputShape *shape = input >= 0 ? &m->input[input] : NULL; - unsigned where = view >= 0 ? IN_VIEW : IN_HANDLER; + unsigned where = view >= 0 ? IN_VIEW | (row >= 0 ? IN_ROW : 0) : IN_HANDLER; if (shape && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0); if (shape && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT; unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK], target[PURE_MODULE_CODE + 1] = {0}; + Join joins[PURE_MODULE_JOINS]; + unsigned open = 0; int height = 0; bool reachable = true; for (uint32_t i = 0; i <= block.length; ++i) { uint32_t pc = block.start + i; - if (!reachable && target[i]) { reachable = true; height = 0; } - else if (target[i] && height) return reject(error, size, "join with a non-empty stack", pc); + /* Branches here brought an empty stack (target) or the stack of a join. */ + unsigned join = open; + for (unsigned k = 0; k < open; ++k) if (joins[k].target == pc) join = k; + if (join < open) { + if (target[i] || (reachable && !same_stack(&joins[join], height, types, owners))) + return reject(error, size, "join with different stacks", pc); + height = joins[join].height; + memcpy(types, joins[join].types, (size_t)height); + memcpy(owners, joins[join].owners, (size_t)height); + joins[join] = joins[--open]; + reachable = true; + } else if (target[i]) { + if (reachable && height) return reject(error, size, "join with different stacks", pc); + height = 0; + reachable = true; + } if (i == block.length) break; if (!reachable) return reject(error, size, "unreachable instruction", pc); const PureOp *op = &m->code[pc]; if (op->op >= PURE_OP_COUNT) return reject(error, size, "unknown opcode", pc); unsigned allowed = signature[op->op].where; if (!(allowed & where)) { - if (view >= 0) return reject(error, size, "view with an effect or branch", pc); + if (allowed & IN_ROW) return reject(error, size, "row outside a column", pc); + if (view >= 0) return reject(error, size, "view with an effect", pc); if (allowed & IN_KEYED) return reject(error, size, "document operation outside a keyed message handler", pc); if (allowed & IN_TEXT) return reject(error, size, "text input outside a text handler", pc); return reject(error, size, "message operation outside a message handler", pc); @@ -228,6 +256,7 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ item_field(arg) < m->record[m->state_ref[list_of(arg)]].count; break; case PURE_OP_INDEX_LOAD: case PURE_OP_INDEX_STORE: known = state_of(m, arg, PURE_STATE_INDEX); break; + case PURE_OP_ROW: known = (int)arg == row; break; default: break; } if (!known) return reject(error, size, "operand names no such state, field, constant or unit", pc); @@ -275,7 +304,7 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ if (height == PURE_MODULE_STACK) return reject(error, size, "stack overflow", pc); owners[height] = (unsigned char)(op->op == PURE_OP_CUR_LOAD || op->op == PURE_OP_INDEX_LOAD ? m->state_ref[arg] : op->op == PURE_OP_CUR_START || op->op == PURE_OP_CUR_END || - op->op == PURE_OP_INDEX_FIRST || op->op == PURE_OP_INDEX_LAST ? arg : + op->op == PURE_OP_INDEX_FIRST || op->op == PURE_OP_INDEX_LAST || op->op == PURE_OP_ROW ? arg : op->op == PURE_OP_INDEX_MOVE ? index_owner : owner >= 0 ? (unsigned)owner : 0); types[height++] = out; @@ -283,8 +312,20 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ if (op->op == PURE_OP_JZ || op->op == PURE_OP_JMP) { if (arg <= pc || arg > block.start + block.length) return reject(error, size, "branch not forward within its block", pc); - if (height) return reject(error, size, "branch with a non-empty stack", pc); - target[arg - block.start] = 1; + unsigned join = open; + for (unsigned k = 0; k < open; ++k) if (joins[k].target == arg) join = k; + if (!height) { + if (join < open) return reject(error, size, "join with different stacks", pc); + target[arg - block.start] = 1; + } else if (join < open) { + if (!same_stack(&joins[join], height, types, owners)) return reject(error, size, "join with different stacks", pc); + } else { + if (target[arg - block.start]) return reject(error, size, "join with different stacks", pc); + if (open == PURE_MODULE_JOINS) return reject(error, size, "more than 8 joins ahead", pc); + joins[open] = (Join){.target = arg, .height = height}; + memcpy(joins[open].types, types, (size_t)height); + memcpy(joins[open++].owners, owners, (size_t)height); + } if (op->op == PURE_OP_JMP) reachable = false; } } @@ -446,12 +487,22 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, (out->state_type[i] == PURE_STATE_LIST && out->state_ref[i] >= out->records) || (out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST))) return reject(error, error_size, "state reference", i), PURE_INVALID; + /* A table names its list in place of code; its columns follow it. */ for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) { if (!name_at(text, strings, read32(at), true, out->view_names[i])) return reject(error, error_size, "view name", i), PURE_INVALID; - if (read32(at + 4) > PURE_VIEW_TEXT) return reject(error, error_size, "view type", i), PURE_INVALID; - out->view_type[i] = (uint8_t)read32(at + 4); + out->view_type[i] = at[4]; + out->view_table[i] = (uint16_t)read16(at + 6); out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)}; + uint32_t table = out->view_table[i]; + if (at[4] > PURE_VIEW_TABLE || at[5] || (table && (table > i || out->view_type[table - 1] != PURE_VIEW_TABLE || + at[4] == PURE_VIEW_TABLE))) + return reject(error, error_size, "view type", i), PURE_INVALID; + if (at[4] == PURE_VIEW_TABLE) { + if (!state_of(out, out->view_code[i].start, PURE_STATE_LIST) || out->view_code[i].length) + return reject(error, error_size, "table list", i), PURE_INVALID; + out->view_list[i] = (uint16_t)out->view_code[i].start; + } else if (table) out->view_list[i] = out->view_list[table - 1]; } for (uint32_t i = 0; i < out->handlers; ++i, at += HANDLER_BYTES) { uint32_t event = read32(at); @@ -472,14 +523,22 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, for (uint32_t i = 0; i < out->records; ++i) names[count++] = out->record_names[i]; for (uint32_t i = 0; i < out->collections; ++i) names[count++] = out->collection_names[i]; for (uint32_t i = 0; i < out->inputs; ++i) names[count++] = out->input_names[i]; - for (uint32_t i = 0; i < out->views; ++i) names[count++] = out->view_names[i]; + for (uint32_t i = 0; i < out->views; ++i) if (!out->view_table[i]) names[count++] = out->view_names[i]; for (size_t i = 0; i < count; ++i) for (size_t k = 0; k < i; ++k) if (!strcmp(names[i], names[k])) return reject(error, error_size, "duplicate name", (unsigned)i), PURE_INVALID; + /* Column names are unique within their table. */ for (uint32_t i = 0; i < out->views; ++i) - if (!verify_block(out, out->view_code[i], out->view_type[i], 0, error, error_size, i)) return PURE_INVALID; + for (uint32_t k = 0; k < i; ++k) + if (out->view_table[i] && out->view_table[k] == out->view_table[i] && + !strcmp(out->view_names[i], out->view_names[k])) + return reject(error, error_size, "duplicate column", i), PURE_INVALID; + for (uint32_t i = 0; i < out->views; ++i) + if (out->view_type[i] != PURE_VIEW_TABLE && + !verify_block(out, out->view_code[i], out->view_type[i], out->view_table[i] ? out->view_list[i] : -1, 0, + error, error_size, i)) return PURE_INVALID; for (uint32_t i = 0; i < out->handlers; ++i) - if (!verify_block(out, out->handler[i].code, -1, out->handler[i].event, error, error_size, i)) + if (!verify_block(out, out->handler[i].code, -1, -1, out->handler[i].event, error, error_size, i)) return PURE_INVALID; return PURE_OK; } @@ -492,7 +551,17 @@ static int find(const char (*names)[PURE_MODULE_NAME], unsigned count, const cha int pure_module_state(const PureModule *m, const char *name) { return find(m->state_names, m->states, name); } int pure_module_input(const PureModule *m, const char *name) { return find(m->input_names, m->inputs, name); } -int pure_module_view(const PureModule *m, const char *name) { return find(m->view_names, m->views, name); } +int pure_module_view(const PureModule *m, const char *name) { + for (uint32_t i = 0; i < m->views; ++i) + if (!m->view_table[i] && !strcmp(m->view_names[i], name)) return (int)i; + return -1; +} + +int pure_module_column(const PureModule *m, uint32_t table, const char *name) { + for (uint32_t i = 0; i < m->views; ++i) + if (m->view_table[i] == table + 1 && !strcmp(m->view_names[i], name)) return (int)i; + return -1; +} /* ---- Execution ------------------------------------------------------ */ @@ -506,6 +575,7 @@ typedef struct { const PureInputShape *shape; /* message, or NULL */ PureArena *arena; double dt; + size_t row; /* the row a column is evaluated for */ bool modified, exists; } Event; @@ -917,6 +987,11 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even continue; } case PURE_OP_INDEX_NUM: ++pc; continue; /* the position itself */ + case PURE_OP_ROW: + stack[sp].num = (double)ev->row; + stack[sp++].bytes.owner = (uint16_t)op->arg; + ++pc; + continue; default: break; } b = stack[--sp].num; @@ -1133,11 +1208,11 @@ PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), v } /* A view that fails reports it by its result; the fault stays the last event's. */ -static PureResult evaluate(PureApp *app, uint32_t view, Slot *out) { +static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) { double values[PURE_MODULE_STATES]; memcpy(values, app->values, sizeof(values)); app->view_arena.used = 0; - Event ev = {.app = app, .arena = &app->view_arena}; + Event ev = {.app = app, .arena = &app->view_arena, .row = row}; PureFault fault = app->fault; uint32_t fault_pc = app->fault_pc; PureResult result = run(app->module, values, app->module->view_code[view], &ev, out); @@ -1146,18 +1221,43 @@ static PureResult evaluate(PureApp *app, uint32_t view, Slot *out) { return result; } +/* A view outside a table (column false), or a column and a row of its table. */ +static PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type, Slot *slot) { + const PureModule *m = app->module; + if (view >= m->views || m->view_type[view] != type || (m->view_table[view] != 0) != column) return PURE_INVALID; + if (column && row >= pure_list_count(app->lists[m->view_list[view]])) return PURE_NOT_FOUND; + return evaluate(app, view, row, slot); +} + bool pure_app_view(PureApp *app, uint32_t view, double *out) { - if (view >= app->module->views || app->module->view_type[view] != PURE_VIEW_NUM) return false; Slot slot; - if (evaluate(app, view, &slot) != PURE_OK) return false; + if (view_of(app, view, false, 0, PURE_VIEW_NUM, &slot) != PURE_OK) return false; *out = slot.num; return true; } PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *, PureSpan), void *context) { - if (view >= app->module->views || app->module->view_type[view] != PURE_VIEW_TEXT) return PURE_INVALID; Slot slot; - PureResult result = evaluate(app, view, &slot); + PureResult result = view_of(app, view, false, 0, PURE_VIEW_TEXT, &slot); + return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result; +} + +size_t pure_app_rows(const PureApp *app, uint32_t table) { + const PureModule *m = app->module; + if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return 0; + return pure_list_count(app->lists[m->view_list[table]]); +} + +bool pure_app_cell(PureApp *app, uint32_t column, size_t row, double *out) { + Slot slot; + if (view_of(app, column, true, row, PURE_VIEW_NUM, &slot) != PURE_OK) return false; + *out = slot.num; + return true; +} + +PureResult pure_app_cell_text(PureApp *app, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context) { + Slot slot; + PureResult result = view_of(app, column, true, row, PURE_VIEW_TEXT, &slot); return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result; } diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h index 6774cca..749f9d0 100644 --- a/prototypes/connections/module.h +++ b/prototypes/connections/module.h @@ -8,18 +8,18 @@ #include <stddef.h> #include <stdint.h> -/* Pure module format v4 (docs/design/008-module-format.md). A toolchain such +/* Pure module format v5 (docs/design/008-module-format.md). A toolchain such * as aup produces it; Pure verifies it completely before anything executes: * operand types (including which text a cursor and which list an index - * belongs to), stack heights, forward-only branches, names, records, - * constants and effects. */ + * belongs to), stack heights and the stack at every join, forward-only + * branches, names, records, tables, constants and effects. */ enum { - PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 24, + PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 64, PURE_MODULE_HANDLERS = 64, PURE_MODULE_CODE = 512, PURE_MODULE_STACK = 64, PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 40, PURE_MODULE_FIELDS = 32, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 64, PURE_MODULE_CONSTANT_BYTES = 8192, PURE_MODULE_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64, - PURE_RECORD_FIELDS = 16 + PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8 }; typedef enum { @@ -36,7 +36,7 @@ typedef enum { PURE_OP_TEXT_SET, PURE_OP_TEXT_FROM, PURE_OP_BYTES_FROM, PURE_OP_CUR_COUNT, PURE_OP_LIST_COUNT, PURE_OP_LIST_APPEND, PURE_OP_LIST_INSERT, PURE_OP_LIST_REMOVE, PURE_OP_ITEM_GET, PURE_OP_ITEM_SET, PURE_OP_INDEX_LOAD, PURE_OP_INDEX_STORE, - PURE_OP_INDEX_MOVE, PURE_OP_INDEX_FIRST, PURE_OP_INDEX_LAST, PURE_OP_INDEX_NUM, + PURE_OP_INDEX_MOVE, PURE_OP_INDEX_FIRST, PURE_OP_INDEX_LAST, PURE_OP_INDEX_NUM, PURE_OP_ROW, PURE_OP_COUNT } PureOpcode; @@ -45,7 +45,9 @@ typedef enum { PURE_FIELD_U32 = 1, PURE_FIELD_BYTES = 2 } PureFieldType; typedef enum { PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4 } PureStateType; -typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1 } PureViewType; +/* A table view has one row per item of a list; its columns are number or + * text views evaluated for one row at a time. */ +typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2 } PureViewType; typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit; typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge; typedef enum { @@ -81,6 +83,8 @@ typedef struct { PureField field[PURE_MODULE_FIELDS]; double initial[PURE_MODULE_STATES]; uint8_t view_type[PURE_MODULE_VIEWS]; + uint16_t view_table[PURE_MODULE_VIEWS]; /* a column's table view + 1, or 0 */ + uint16_t view_list[PURE_MODULE_VIEWS]; /* the list of a table or column */ PureBlock view_code[PURE_MODULE_VIEWS]; PureHandler handler[PURE_MODULE_HANDLERS]; PureOp code[PURE_MODULE_CODE]; @@ -96,10 +100,11 @@ typedef struct { /* Decode and verify. On failure out is unspecified and error names the rule. */ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, char *error, size_t error_size); -/* Index of a name, or -1. */ +/* Index of a name, or -1. Views are those outside tables. */ int pure_module_state(const PureModule *, const char *name); int pure_module_input(const PureModule *, const char *name); int pure_module_view(const PureModule *, const char *name); +int pure_module_column(const PureModule *, uint32_t table, const char *name); /* Keyed collections live in a data host (for example the tree store). One * event opens at most one document, the message's key, at its current @@ -163,6 +168,11 @@ void pure_app_finish(PureApp *); bool pure_app_view(PureApp *, uint32_t view, double *out); /* A text view, lent in order until the next view evaluation. */ PureResult pure_app_view_text(PureApp *, uint32_t view, bool (*visit)(void *, PureSpan), void *context); +/* The rows of a table view (its list's items), or 0 for another view. A + * host evaluates only the cells it shows. */ +size_t pure_app_rows(const PureApp *, uint32_t table); +bool pure_app_cell(PureApp *, uint32_t column, size_t row, double *out); +PureResult pure_app_cell_text(PureApp *, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context); /* Switch to another verified module. States keep their value by name (text * states keep their content, cursors their position in that text); new states * start at their initial value. The module must outlive the app. */ diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c index 7f9f316..8a9788f 100644 --- a/prototypes/connections/pure-run.c +++ b/prototypes/connections/pure-run.c @@ -8,7 +8,8 @@ * JSON snapshot per command. Not a product interface. * down NAME | up NAME | frame [N] | type NAME HEX|- | replace PATH | show * Text states and text views print as JSON strings; cursors as byte positions; - * lists as arrays of objects; indices as positions. A failed view is null. */ + * lists as arrays of objects; indices as positions; a table view as an array + * with an object of its columns per row. A failed view or cell is null. */ static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp", "limit", "not_found", "encoding"}; static bool load(const char *path, PureModule *module) { @@ -67,6 +68,27 @@ static void show_list(const PureModule *m, const PureList *list, const PureRecor putchar(']'); } +/* A number or text view, or a column at a row. */ +static void show_value(PureApp *app, uint32_t view, size_t row) { + const PureModule *m = app->module; + static Collected text; + text.used = 0; + text.fits = true; + double value; + bool column = m->view_table[view]; + if (m->view_type[view] == PURE_VIEW_TEXT) { + PureResult result = column ? pure_app_cell_text(app, view, row, collect, &text) + : pure_app_view_text(app, view, collect, &text); + if (result != PURE_OK || !text.fits) printf("null"); + else { + putchar('"'); + json_span(NULL, (PureSpan){text.bytes, text.used}); + putchar('"'); + } + } else if (column ? pure_app_cell(app, view, row, &value) : pure_app_view(app, view, &value)) printf("%.17g", value); + else printf("null"); +} + static void show(PureApp *app, const char *failure) { const PureModule *m = app->module; printf("{\"states\":{"); @@ -80,21 +102,23 @@ static void show(PureApp *app, const char *failure) { else printf("%.17g", app->values[i]); } printf("},\"views\":{"); + bool first = true; for (uint32_t i = 0; i < m->views; ++i) { - printf("%s\"%s\":", i ? "," : "", m->view_names[i]); - double value; - static Collected text; - text.used = 0; - text.fits = true; - if (m->view_type[i] == PURE_VIEW_TEXT) { - if (pure_app_view_text(app, i, collect, &text) != PURE_OK || !text.fits) printf("null"); - else { - putchar('"'); - json_span(NULL, (PureSpan){text.bytes, text.used}); - putchar('"'); + if (m->view_table[i]) continue; + printf("%s\"%s\":", first ? "" : ",", m->view_names[i]); + first = false; + if (m->view_type[i] != PURE_VIEW_TABLE) { show_value(app, i, 0); continue; } + putchar('['); + for (size_t row = 0; row < pure_app_rows(app, i); ++row) { + printf("%s{", row ? "," : ""); + for (uint32_t c = 0, shown = 0; c < m->views; ++c) { + if (m->view_table[c] != i + 1) continue; + printf("%s\"%s\":", shown++ ? "," : "", m->view_names[c]); + show_value(app, c, row); } - } else if (pure_app_view(app, i, &value)) printf("%.17g", value); - else printf("null"); + putchar('}'); + } + putchar(']'); } printf("},\"tick\":%llu,\"successfulEvents\":%llu,\"fault\":\"%s\",\"error\":%s%s%s}\n", (unsigned long long)app->ticks, (unsigned long long)app->events, fault_names[app->fault], diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c index 8867e0a..3c12d49 100644 --- a/prototypes/connections/test-module.c +++ b/prototypes/connections/test-module.c @@ -23,11 +23,14 @@ typedef struct { const char *name; uint32_t type, ref; } T; * default is the constant numbered initial. */ typedef struct { const char *name; uint32_t text; double initial; } RF; typedef struct { const char *name; const RF *fields; size_t nfields; } R; +/* A further view: a table (type 2, start = list state) or a column of the + * view numbered table - 1. */ +typedef struct { const char *name; uint32_t type, table, start, length; } W; typedef struct { const S *s; size_t ns; const I *in; size_t ni; const V *v; size_t nv; const H *h; size_t nh; const O *o; size_t no; const char *const *c; size_t nc; const T *t; size_t nt; const char *const *k; size_t nk; const V *tv; size_t ntv; - const R *r; size_t nr; + const R *r; size_t nr; const W *w; size_t nw; } Spec; static unsigned char *put32(unsigned char *p, uint32_t v) { @@ -42,7 +45,7 @@ static unsigned char *put_double(unsigned char *p, double value) { return put32(p + 4, (uint32_t)(bits >> 32)); } -/* Test-side encoder for format v4; the toolchain has its own. */ +/* Test-side encoder for format v5; the toolchain has its own. */ static size_t build(unsigned char *out, Spec spec) { unsigned char strings[4096], blob[4096]; size_t used = 0, blob_used = 0; @@ -59,6 +62,7 @@ static size_t build(unsigned char *out, Spec spec) { } for (size_t i = 0; i < spec.nv; ++i) text[names++] = spec.v[i].name; for (size_t i = 0; i < spec.ntv; ++i) text[names++] = spec.tv[i].name; + for (size_t i = 0; i < spec.nw; ++i) text[names++] = spec.w[i].name; size_t record_names = names, record_fields = 0; for (size_t i = 0; i < spec.nr; ++i) { text[names++] = spec.r[i].name; @@ -77,8 +81,8 @@ static size_t build(unsigned char *out, Spec spec) { for (size_t i = 0; i < spec.nk; ++i) { memcpy(blob + blob_used, spec.k[i], strlen(spec.k[i])); blob_used += strlen(spec.k[i]); } unsigned char *p = out; memset(p, 0, 40); - memcpy(p, "PUREMOD4", 8); - size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv; + memcpy(p, "PUREMOD5", 8); + size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv + spec.nw; p[8] = (unsigned char)states; p[9] = (unsigned char)(states >> 8); p[10] = (unsigned char)spec.ni; p[12] = (unsigned char)views; p[14] = (unsigned char)spec.nh; put32(p + 16, (uint32_t)spec.no); @@ -128,6 +132,13 @@ static size_t build(unsigned char *out, Spec spec) { } for (size_t i = 0; i < spec.nv; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put32(p, spec.v[i].start); p = put32(p, spec.v[i].length); } for (size_t i = 0; i < spec.ntv; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, spec.tv[i].start); p = put32(p, spec.tv[i].length); } + for (size_t i = 0; i < spec.nw; ++i) { + const W *w = &spec.w[i]; + p = put32(p, offset[n++]); + *p++ = (unsigned char)w->type; *p++ = 0; *p++ = (unsigned char)w->table; *p++ = (unsigned char)(w->table >> 8); + p = put32(p, w->start); + p = put32(p, w->length); + } for (size_t i = 0; i < spec.nh; ++i) { p = put32(p, spec.h[i].event); p = put32(p, spec.h[i].start); p = put32(p, spec.h[i].length); } for (size_t i = 0; i < spec.no; ++i) { *p++ = spec.o[i].op; *p++ = 0; *p++ = 0; *p++ = 0; @@ -149,7 +160,7 @@ static PureResult load(Spec spec, PureModule *out) { } #define LEN(a) (sizeof(a) / sizeof(*(a))) -#define SPEC(s, in, v, h, o) ((Spec){s, LEN(s), in, LEN(in), v, LEN(v), h, LEN(h), o, LEN(o), NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}) +#define SPEC(s, in, v, h, o) ((Spec){s, LEN(s), in, LEN(in), v, LEN(v), h, LEN(h), o, LEN(o), NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}) enum { C = PURE_OP_CONST, L = PURE_OP_LOAD, DT = PURE_OP_DT, ADD = PURE_OP_ADD, MUL = PURE_OP_MUL, DIV = PURE_OP_DIV, GT = PURE_OP_GT, MIN = PURE_OP_MIN, @@ -204,7 +215,7 @@ static void behaviour(void) { static const V no_views[1]; static const H no_handlers[1]; static const O no_code[1]; - CHECK(load((Spec){next_states, 2, no_inputs, 0, no_views, 0, no_handlers, 0, no_code, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_b) == PURE_OK); + CHECK(load((Spec){next_states, 2, no_inputs, 0, no_views, 0, no_handlers, 0, no_code, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_b) == PURE_OK); CHECK(pure_app_replace(&app, &module_b) == PURE_OK); CHECK(app.module == &module_b && app.values[0] == 7 && app.values[1] == 25); CHECK(pure_app_replace(&app, &module_a) == PURE_OK); @@ -280,28 +291,28 @@ static void rejections(void) { for (int i = 0; i < 65; ++i) overflow[i] = (O){C, 0, 1}; overflow[65] = (O){ST, 0, 0}; const Rejection cases[] = { - {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"state name", {bad_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"state name", {long_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"non-finite initial value", {non_finite, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"input key", {s1, 1, bad_key, 1, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"duplicate key", {s1, 1, dup_key, 2, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"duplicate name", {s1, 1, i1, 0, clash, 1, none, 0, load_a, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"view with an effect", {s1, 1, i1, 0, v_store, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"view with an effect", {s1, 1, i1, 0, v_dt, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"empty view", {s1, 1, i1, 0, v_empty, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"handler event", {s1, 1, i1, 1, clash, 0, h_bad_event, 1, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, back, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, beyond, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"stack underflow", {s1, 1, i1, 0, clash, 0, h_all, 1, under, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"join with a non-empty stack", {s1, 1, i1, 0, clash, 0, h_five, 1, join, 5, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"handler must leave no value", {s1, 1, i1, 0, clash, 0, h_two, 1, leftover, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"no such state", {s1, 1, i1, 0, clash, 0, h_two, 1, unknown_state, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h_two, 1, nan_const, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h0, 1, noncanon, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"unreachable instruction", {s1, 1, i1, 0, clash, 0, h_four, 1, unreachable, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"unknown opcode", {s1, 1, i1, 0, clash, 0, h_two, 1, bad_op, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, - {"stack overflow", {s1, 1, i1, 0, clash, 0, (H[]){{0, 0, 66}}, 1, overflow, 66, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"state name", {bad_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"state name", {long_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"non-finite initial value", {non_finite, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"input key", {s1, 1, bad_key, 1, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"duplicate key", {s1, 1, dup_key, 2, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"duplicate name", {s1, 1, i1, 0, clash, 1, none, 0, load_a, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"view with an effect", {s1, 1, i1, 0, v_store, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"view with an effect", {s1, 1, i1, 0, v_dt, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"empty view", {s1, 1, i1, 0, v_empty, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"handler event", {s1, 1, i1, 1, clash, 0, h_bad_event, 1, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, back, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, beyond, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"stack underflow", {s1, 1, i1, 0, clash, 0, h_all, 1, under, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"join with different stacks", {s1, 1, i1, 0, clash, 0, h_five, 1, join, 5, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"handler must leave no value", {s1, 1, i1, 0, clash, 0, h_two, 1, leftover, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"no such state", {s1, 1, i1, 0, clash, 0, h_two, 1, unknown_state, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h_two, 1, nan_const, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h0, 1, noncanon, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"unreachable instruction", {s1, 1, i1, 0, clash, 0, h_four, 1, unreachable, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"unknown opcode", {s1, 1, i1, 0, clash, 0, h_two, 1, bad_op, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, + {"stack overflow", {s1, 1, i1, 0, clash, 0, (H[]){{0, 0, 66}}, 1, overflow, 66, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID}, }; for (size_t i = 0; i < LEN(cases); ++i) { PureResult result = load(cases[i].spec, &module_a); @@ -313,9 +324,9 @@ static void rejections(void) { static S many[33]; static char many_names[33][8]; for (unsigned i = 0; i < 33; ++i) { snprintf(many_names[i], 8, "s%u", i % 100u); many[i] = (S){many_names[i], 0}; } - CHECK(load((Spec){many, 33, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_a) == PURE_LIMIT); + CHECK(load((Spec){many, 33, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_a) == PURE_LIMIT); static unsigned char bytes[65536]; - size_t size = build(bytes, (Spec){s1, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}); + size_t size = build(bytes, (Spec){s1, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}); CHECK(pure_module_load(bytes, size - 1, &module_a, error, sizeof(error)) == PURE_INVALID); CHECK(pure_module_load(bytes, size + 1, &module_a, error, sizeof(error)) == PURE_INVALID); bytes[0] = 'X'; @@ -527,7 +538,7 @@ static void messages(void) { }; static const H handlers[] = {{1, 0, 2}, {3, 2, 12}, {5, 14, 6}, {7, 20, 14}}; static const V views[] = {{"count", 0, 0}}; - Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; editor_spec = spec; PureResult loaded = load(spec, &module_a); if (loaded != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } @@ -593,23 +604,28 @@ static void messages(void) { static const I unkeyed[] = {{"note", NULL, PURE_KIND_MESSAGE, false, NULL, 0}}; static const O doc_read[] = {{DOC, 0, 0}, {REPLY, 0, 0}}; static const H note_handler[] = {{1, 0, 2}}; - bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "document operation outside")); static const F tail_first[] = {{"insert", 2}, {"start", 1}}; static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, true, tail_first, 2}}; - bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message field")); static const H even[] = {{2, 0, 0}}; - bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "handler event")); static const V field_view[] = {{"v", 0, 1}}; static const O field_only[] = {{FIELD, 0, 0}}; - bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect")); + /* A value may cross a join when every path brings the same stack. */ static const O branch_value[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 3, 0}, {ST, 0, 0}}; + static const O branch_mismatch[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 4, 0}, {C, 0, 2}, {ST, 0, 0}}; static const H frame_handler[] = {{0, 0, 4}}; - bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; - CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "branch with a non-empty stack")); + bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + CHECK(load(bad, &module_b) == PURE_OK); + static const H frame_five[] = {{0, 0, 5}}; + bad.o = branch_mismatch; bad.no = 5; bad.h = frame_five; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks")); } /* Damaged message modules either fail to load or run messages safely. */ @@ -726,7 +742,7 @@ static void texts(void) { static const V number_views[] = {{"chars", 37, 2}, {"offset", 39, 2}, {"line_no", 41, 2}}; static const V text_views[] = {{"line", 43, 2}, {"header", 45, 9}, {"ratio", 54, 4}}; Spec spec = {nums, 1, inputs, LEN(inputs), number_views, 3, handlers, LEN(handlers), code, LEN(code), - NULL, 0, textual, 3, constants, LEN(constants), text_views, 3, NULL, 0}; + NULL, 0, textual, 3, constants, LEN(constants), text_views, 3, NULL, 0, NULL, 0}; text_spec = spec; if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } PureApp app; @@ -761,7 +777,7 @@ static void texts(void) { static const S fresh[] = {{"fresh", 7}}; static const T kept[] = {{"note", 1, 0}, {"at", 2, 1}}; static const char *const other[] = {"ignored"}; - Spec next = {fresh, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, kept, 2, other, 1, text_views, 0, NULL, 0}; + Spec next = {fresh, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, kept, 2, other, 1, text_views, 0, NULL, 0, NULL, 0}; CHECK(load(next, &module_b) == PURE_OK); CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 3); CHECK(pure_app_replace(&app, &module_b) == PURE_OK); @@ -772,7 +788,7 @@ static void texts(void) { static const T two[] = {{"one", 1, 0}, {"two", 1, 0}, {"c", 2, 1}}; static const O wrong_text[] = {{CUL, 3, 0}, {TK, 0, 0}, {TINS, 2, 0}}; static const H frame[] = {{0, 0, 3}}; - Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, wrong_text, 3, NULL, 0, two, 3, constants, 1, text_views, 0, NULL, 0}; + Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, wrong_text, 3, NULL, 0, two, 3, constants, 1, text_views, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "cursor of another text")); static const O input_in_frame[] = {{CUL, 3, 0}, {TI, 0, 0}, {TINS, 1, 0}}; bad.o = input_in_frame; @@ -786,13 +802,13 @@ static void texts(void) { CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state")); static const V text_as_number[] = {{"v", 0, 1}}; static const O constant_only[] = {{TK, 0, 0}}; - bad = (Spec){nums, 1, inputs, 0, text_as_number, 1, NULL, 0, constant_only, 1, NULL, 0, two, 3, constants, 1, NULL, 0, NULL, 0}; + bad = (Spec){nums, 1, inputs, 0, text_as_number, 1, NULL, 0, constant_only, 1, NULL, 0, two, 3, constants, 1, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view must leave one value of its type")); static const T cursor_on_number[] = {{"c", 2, 0}}; - bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, cursor_on_number, 1, constants, 1, NULL, 0, NULL, 0}; + bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, cursor_on_number, 1, constants, 1, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference")); static const char *const broken[] = {"\xc0\xaf"}; - bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0, NULL, 0}; + bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text constant")); } @@ -908,7 +924,7 @@ static void lists(void) { static const V number_views[] = {{"count", 46, 1}, {"position", 47, 2}, {"done", 49, 2}}; static const V text_views[] = {{"current", 51, 2}}; Spec spec = {nums, 1, inputs, LEN(inputs), number_views, 3, handlers, LEN(handlers), code, LEN(code), - NULL, 0, states, 2, constants, 3, text_views, 1, records, 1}; + NULL, 0, states, 2, constants, 3, text_views, 1, records, 1, NULL, 0}; list_spec = spec; if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } PureApp app; @@ -951,7 +967,7 @@ static void lists(void) { * fields their default. */ static const S no_nums[] = {{"made", 0}}; Spec same = {no_nums, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, states, 2, constants, 3, - text_views, 0, records, 1}; + text_views, 0, records, 1, NULL, 0}; CHECK(load(same, &module_b) == PURE_OK); const PureList *before = app.lists[1]; CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.lists[1] == before && app.values[2] == 2); @@ -980,7 +996,7 @@ static void lists(void) { static const O other_index[] = {{XL, 3, 0}, {LR, 2, 0}}; static const H frame[] = {{0, 0, 2}}; Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, other_index, 2, NULL, 0, two_lists, 3, constants, 3, - text_views, 0, records, 1}; + text_views, 0, records, 1, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "index of another list")); static const O swapped[] = {{C, 0, 0}, {TK, 0, 0}, {LA, 1, 0}}; static const H frame3[] = {{0, 0, 3}}; @@ -997,11 +1013,11 @@ static void lists(void) { CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type")); static const V append_view[] = {{"v", 0, 3}}; bad = (Spec){nums, 1, inputs, 0, append_view, 1, NULL, 0, (O[]){{TK, 0, 0}, {C, 0, 0}, {LA, 1, 0}}, 3, NULL, 0, - two_lists, 3, constants, 3, NULL, 0, records, 1}; + two_lists, 3, constants, 3, NULL, 0, records, 1, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect")); static const T index_of_number[] = {{"ia", 4, 0}}; bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, index_of_number, 1, constants, 3, NULL, 0, - records, 1}; + records, 1, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference")); static const T list_of_nothing[] = {{"a", 3, 1}}; bad.t = list_of_nothing; @@ -1073,6 +1089,150 @@ static void list_mutations(void) { printf("{\"list_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); } +static Spec table_spec; + +static const char *cell_text(PureApp *app, uint32_t column, size_t row) { + static Text t; + t.size = 0; + t.text[0] = 0; + CHECK(pure_app_cell_text(app, column, row, text_span, &t) == PURE_OK); + return t.text; +} + +enum { ROW = PURE_OP_ROW, EQ = PURE_OP_EQ, LT = PURE_OP_LT }; + +/* A table view has a row per item; its columns read the row and may branch. + * A value crosses a join when every path brings the same stack. */ +static void tables(void) { + static const S nums[] = {{"n", 0}}; + static const T states[] = {{"tasks", 3, 0}, {"at", 4, 1}}; + static const RF task_fields[] = {{"title", 1, 0}, {"done", 0, 0}}; + static const R records[] = {{"task", task_fields, 2}}; + static const char *const constants[] = {"", "x", " ", "empty"}; + static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "Space")}; + static const O code[] = { + {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, /* 0 typed */ + {XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 5 toggle */ + {LC, 1, 0}, {C, 0, 0}, {GT, 0, 0}, {JZ, 17, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {JMP, 18, 0}, + {TK, 3, 0}, /* 10 current */ + {ROW, 1, 0}, {IG, TITLE, 0}, /* 18 title */ + {ROW, 1, 0}, {IG, DONE, 0}, {JZ, 25, 0}, {TK, 1, 0}, {JMP, 26, 0}, {TK, 2, 0}, /* 20 mark */ + {ROW, 1, 0}, {XN, 0, 0}, /* 26 pos */ + {ROW, 1, 0}, {XN, 0, 0}, {XL, 2, 0}, {XN, 0, 0}, {EQ, 0, 0}, /* 28 here */ + }; + static const H handlers[] = {{1, 0, 5}, {3, 5, 5}}; + static const V text_views[] = {{"current", 10, 8}}; + static const W table[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"mark", 1, 2, 20, 6}, + {"pos", 0, 2, 26, 2}, {"here", 0, 2, 28, 5}}; + Spec spec = {nums, 1, inputs, 2, NULL, 0, handlers, 2, code, LEN(code), NULL, 0, states, 2, constants, 4, + text_views, 1, records, 1, table, 5}; + table_spec = spec; + if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } + CHECK(pure_module_view(&module_a, "board") == 1 && pure_module_view(&module_a, "title") == -1 && + pure_module_column(&module_a, 1, "title") == 2 && pure_module_column(&module_a, 1, "current") == -1); + PureApp app; + CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK); + double value; + CHECK(pure_app_rows(&app, 1) == 0 && !strcmp(view_text(&app, 0), "empty")); /* the else branch */ + CHECK(pure_app_cell_text(&app, 2, 0, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"a", 1}) == PURE_OK); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xeb\xb9\xb5", 3}) == PURE_OK); + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); + CHECK(pure_app_rows(&app, 1) == 2 && !strcmp(view_text(&app, 0), "\xeb\xb9\xb5")); + CHECK(!strcmp(cell_text(&app, 2, 0), "a") && !strcmp(cell_text(&app, 2, 1), "\xeb\xb9\xb5")); + CHECK(!strcmp(cell_text(&app, 3, 0), " ") && !strcmp(cell_text(&app, 3, 1), "x")); + CHECK(pure_app_cell(&app, 4, 1, &value) && value == 1 && pure_app_cell(&app, 5, 0, &value) && value == 0 && + pure_app_cell(&app, 5, 1, &value) && value == 1); + CHECK(!pure_app_cell(&app, 4, 2, &value) && pure_app_cell_text(&app, 2, 2, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND); + /* Columns are read by row, other views without one. */ + CHECK(!pure_app_view(&app, 4, &value) && pure_app_view_text(&app, 2, text_span, &(Text){{0}, 0}) == PURE_INVALID); + CHECK(pure_app_cell_text(&app, 0, 0, text_span, &(Text){{0}, 0}) == PURE_INVALID && pure_app_rows(&app, 0) == 0); + CHECK(pure_app_view_text(&app, 1, text_span, &(Text){{0}, 0}) == PURE_INVALID); + pure_app_stop(&app); + + /* Verification of rows, tables and joins. */ + Spec bad = spec; + static const V row_view[] = {{"current", 18, 2}}; + bad.tv = row_view; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "row outside a column")); + static const H row_handler[] = {{3, 26, 2}}; + bad = spec; bad.h = row_handler; bad.nh = 1; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "row outside a column")); + static const W twice[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"title", 1, 2, 18, 2}}; + bad = spec; bad.w = twice; bad.nw = 3; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate column")); + static const W two_tables[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"other", 2, 0, 1, 0}, + {"title", 1, 4, 18, 2}, {"tasks", 0, 4, 26, 2}}; + bad = spec; bad.w = two_tables; bad.nw = 5; + CHECK(load(bad, &module_b) == PURE_OK); /* column names belong to their table */ + static const W not_a_table[] = {{"title", 1, 1, 18, 2}}; + bad = spec; bad.w = not_a_table; bad.nw = 1; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view type")); + static const W over_number[] = {{"board", 2, 0, 0, 0}}; + bad = spec; bad.w = over_number; bad.nw = 1; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "table list")); + static const O other_row[] = {{ROW, 2, 0}, {XN, 0, 0}}; + static const W other_row_column[] = {{"board", 2, 0, 1, 0}, {"pos", 0, 1, 0, 2}}; + bad = spec; bad.o = other_row; bad.no = 2; bad.h = NULL; bad.nh = 0; bad.tv = NULL; bad.ntv = 0; + bad.w = other_row_column; bad.nw = 2; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state")); + static const O typed_join[] = {{L, 0, 0}, {JZ, 4, 0}, {TK, 0, 0}, {JMP, 5, 0}, {C, 0, 1}}; + static const V mixed[] = {{"current", 0, 5}}; + bad = spec; bad.o = typed_join; bad.no = 5; bad.h = NULL; bad.nh = 0; bad.tv = mixed; bad.w = NULL; bad.nw = 0; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks")); + /* At most 8 joins with a value may be ahead at once. */ + for (unsigned open = 8; open <= 9; ++open) { + static O nested[1 + 2 * 9 + 9 + 1]; + unsigned n = 0; + nested[n++] = (O){C, 0, 1}; + for (unsigned k = 0; k < open; ++k) { nested[n++] = (O){C, 0, 0}; nested[n++] = (O){JZ, 1 + 2 * open + k, 0}; } + for (unsigned k = 0; k < open; ++k) nested[n++] = (O){NEG, 0, 0}; + nested[n++] = (O){ST, 0, 0}; + H frame = {0, 0, n}; + bad = spec; bad.o = nested; bad.no = n; bad.h = &frame; bad.nh = 1; bad.tv = NULL; bad.ntv = 0; bad.w = NULL; bad.nw = 0; + CHECK(load(bad, &module_b) == (open == 8 ? PURE_OK : PURE_INVALID)); + } + CHECK(strstr(error, "more than 8 joins ahead")); +} + +/* Damaged table modules either fail to load or evaluate every cell safely. */ +static void table_mutations(void) { + static unsigned char valid[65536], bytes[65536]; + size_t size = build(valid, table_spec); + uint32_t state = 0xc2b2ae35u; + unsigned accepted = 0; + for (unsigned round = 0; round < 20000; ++round) { + memcpy(bytes, valid, size); + unsigned flips = 1 + next_random(&state) % 3; + for (unsigned f = 0; f < flips; ++f) + bytes[next_random(&state) % size] ^= (unsigned char)(1u << (next_random(&state) % 8)); + if (pure_module_load(bytes, size, &module_b, error, sizeof(error)) != PURE_OK) continue; + ++accepted; + PureApp app; + if (pure_app_start(&app, &module_b, (PureData){0}) != PURE_OK) continue; + for (unsigned e = 0; e < 12; ++e) { + uint32_t input = next_random(&state) % 2; + if (module_b.input[input].kind == PURE_KIND_TEXT) pure_app_text(&app, input, (PureSpan){(const unsigned char *)"t", 1}); + else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN); + for (uint32_t v = 0; v < module_b.views; ++v) { + double value; + if (!module_b.view_table[v]) { + if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_view_text(&app, v, ignore_span, NULL); + else pure_app_view(&app, v, &value); + continue; + } + size_t rows = pure_app_rows(&app, module_b.view_table[v] - 1u); + for (size_t row = 0; row <= rows; ++row) { + if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_cell_text(&app, v, row, ignore_span, NULL); + else if (pure_app_cell(&app, v, row, &value)) CHECK(isfinite(value)); + } + } + } + pure_app_stop(&app); + } + printf("{\"table_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); +} + int main(void) { behaviour(); faults(); @@ -1084,7 +1244,9 @@ int main(void) { text_mutations(); lists(); list_mutations(); + tables(); + table_mutations(); printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp)); - puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration\"}"); + puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns\"}"); return 0; } |
