aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 13:25:38 +0100
committereunhhu <[email protected]>2026-09-25 13:25:38 +0100
commite22b6f5c51e9133495c632a0c80674c14083c382 (patch)
tree3d5ad8a8331e6a55f0e45bd6ca48fd313fc3ba21 /prototypes
parent567bdf8c459b8cb41a87729ebff493774755945f (diff)
downloadpure-e22b6f5c51e9133495c632a0c80674c14083c382.tar.gz
pure-e22b6f5c51e9133495c632a0c80674c14083c382.zip
Add records, lists and indices to Pure modules
Module format v4 declares record types and adds list states (items of a record) and index states (a position in one list). The verifier types record operands field by field and rejects an index used on another list. Indices follow their item across inserts and removes. List changes are logged per event and undone when the event fails. Replacing a module keeps a list by name and moves its fields by name when the record changed. pure-run prints lists as arrays and a failed view as null.
Diffstat (limited to 'prototypes')
-rw-r--r--prototypes/connections/Makefile6
-rw-r--r--prototypes/connections/module-list.c155
-rw-r--r--prototypes/connections/module-list.h30
-rw-r--r--prototypes/connections/module.c345
-rw-r--r--prototypes/connections/module.h37
-rw-r--r--prototypes/connections/pure-run.c48
-rw-r--r--prototypes/connections/test-module.c329
7 files changed, 858 insertions, 92 deletions
diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile
index 34c789d..9061bf8 100644
--- a/prototypes/connections/Makefile
+++ b/prototypes/connections/Makefile
@@ -115,10 +115,10 @@ $(BUILD)/test-async: test-async.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD
$(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_STORE_TEST test-splice.c program.c $(CORE) $(HOST) $(LDFLAGS) $(LDLIBS) -o $@
-# Pure module format v1: loader/verifier/executor, its tests and a test host.
+# Pure modules: loader/verifier/executor, its tests and a test host.
# Text states are runtime-owned drafts, so the executor links the tree engine.
-MODULE = module.c module-text.c store-tree.c
-MODULE_HEADERS = module.h module-text.h $(HEADERS)
+MODULE = module.c module-list.c module-text.c store-tree.c
+MODULE_HEADERS = module.h module-list.h module-text.h $(HEADERS)
$(BUILD)/test-module: test-module.c $(MODULE) $(MODULE_HEADERS) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) test-module.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@
diff --git a/prototypes/connections/module-list.c b/prototypes/connections/module-list.c
new file mode 100644
index 0000000..68380f2
--- /dev/null
+++ b/prototypes/connections/module-list.c
@@ -0,0 +1,155 @@
+#include "module-list.h"
+#include <stdlib.h>
+#include <string.h>
+
+struct PureList {
+ unsigned fields;
+ uint8_t text_fields[16];
+ PureCell **items;
+ size_t count, capacity;
+};
+
+typedef enum { INSERTED, REMOVED, CHANGED } Change;
+typedef struct {
+ Change change;
+ PureList *list;
+ size_t at;
+ unsigned field;
+ PureCell *item; /* REMOVED: the removed item */
+ PureCell old; /* CHANGED: the replaced cell */
+} Entry;
+
+static void free_item(const PureList *list, PureCell *item) {
+ if (!item) return;
+ for (unsigned f = 0; f < list->fields; ++f)
+ if (list->text_fields[f]) free(item[f].text);
+ free(item);
+}
+
+PureList *pure_list_new(unsigned fields, const uint8_t *text_fields) {
+ if (fields > 16) return NULL;
+ PureList *list = calloc(1, sizeof(*list));
+ if (!list) return NULL;
+ list->fields = fields;
+ memcpy(list->text_fields, text_fields, fields);
+ return list;
+}
+
+void pure_list_free(PureList *list) {
+ if (!list) return;
+ for (size_t i = 0; i < list->count; ++i) free_item(list, list->items[i]);
+ free(list->items);
+ free(list);
+}
+
+size_t pure_list_count(const PureList *list) { return list->count; }
+
+static bool log_change(PureUndo *undo, Entry entry) {
+ if (undo->count == undo->capacity) {
+ size_t capacity = undo->capacity ? 2 * undo->capacity : 16;
+ Entry *entries = realloc(undo->entries, capacity * sizeof(*entries));
+ if (!entries) return false;
+ undo->entries = entries;
+ undo->capacity = capacity;
+ }
+ ((Entry *)undo->entries)[undo->count++] = entry;
+ return true;
+}
+
+static bool copy_cell(const PureList *list, unsigned field, PureCell from, PureCell *to) {
+ *to = (PureCell){.num = list->text_fields[field] ? 0 : from.num};
+ if (!list->text_fields[field] || !from.size) return true;
+ to->text = malloc(from.size);
+ if (!to->text) return false;
+ memcpy(to->text, from.text, from.size);
+ to->size = from.size;
+ return true;
+}
+
+static void place(PureList *list, size_t at, PureCell *item) {
+ memmove(list->items + at + 1, list->items + at, (list->count - at) * sizeof(*list->items));
+ list->items[at] = item;
+ ++list->count;
+}
+
+static PureCell *take(PureList *list, size_t at) {
+ PureCell *item = list->items[at];
+ memmove(list->items + at, list->items + at + 1, (list->count - at - 1) * sizeof(*list->items));
+ --list->count;
+ return item;
+}
+
+PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, PureUndo *undo) {
+ if (at > list->count || list->count == PURE_LIST_ITEMS) return PURE_LIMIT;
+ if (list->count == list->capacity) {
+ size_t capacity = list->capacity ? 2 * list->capacity : 8;
+ PureCell **items = realloc(list->items, capacity * sizeof(*items));
+ if (!items) return PURE_ALLOCATION;
+ list->items = items;
+ list->capacity = capacity;
+ }
+ PureCell *item = calloc(list->fields ? list->fields : 1, sizeof(*item));
+ if (!item) return PURE_ALLOCATION;
+ for (unsigned f = 0; f < list->fields; ++f)
+ if (!copy_cell(list, f, cells[f], &item[f])) { free_item(list, item); return PURE_ALLOCATION; }
+ if (undo && !log_change(undo, (Entry){.change = INSERTED, .list = list, .at = at})) {
+ free_item(list, item);
+ return PURE_ALLOCATION;
+ }
+ place(list, at, item);
+ return PURE_OK;
+}
+
+PureResult pure_list_remove(PureList *list, size_t at, PureUndo *undo) {
+ if (at >= list->count) return PURE_NOT_FOUND;
+ if (!log_change(undo, (Entry){.change = REMOVED, .list = list, .at = at, .item = list->items[at]}))
+ return PURE_ALLOCATION;
+ take(list, at);
+ return PURE_OK;
+}
+
+PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell value, PureUndo *undo) {
+ if (at >= list->count) return PURE_NOT_FOUND;
+ PureCell copy;
+ if (!copy_cell(list, field, value, &copy)) return PURE_ALLOCATION;
+ if (!log_change(undo, (Entry){.change = CHANGED, .list = list, .at = at, .field = field,
+ .old = list->items[at][field]})) {
+ free(copy.text);
+ return PURE_ALLOCATION;
+ }
+ list->items[at][field] = copy;
+ return PURE_OK;
+}
+
+PureCell pure_list_get(const PureList *list, size_t at, unsigned field) {
+ return list->items[at][field];
+}
+
+void pure_undo_rollback(PureUndo *undo) {
+ Entry *entries = undo->entries;
+ for (size_t i = undo->count; i-- > 0;) {
+ Entry *e = &entries[i];
+ if (e->change == INSERTED) free_item(e->list, take(e->list, e->at));
+ else if (e->change == REMOVED) place(e->list, e->at, e->item); /* capacity is still there */
+ else {
+ if (e->list->text_fields[e->field]) free(e->list->items[e->at][e->field].text);
+ e->list->items[e->at][e->field] = e->old;
+ }
+ }
+ undo->count = 0;
+}
+
+void pure_undo_commit(PureUndo *undo) {
+ Entry *entries = undo->entries;
+ for (size_t i = 0; i < undo->count; ++i) {
+ Entry *e = &entries[i];
+ if (e->change == REMOVED) free_item(e->list, e->item);
+ else if (e->change == CHANGED && e->list->text_fields[e->field]) free(e->old.text);
+ }
+ undo->count = 0;
+}
+
+void pure_undo_free(PureUndo *undo) {
+ free(undo->entries);
+ *undo = (PureUndo){0};
+}
diff --git a/prototypes/connections/module-list.h b/prototypes/connections/module-list.h
new file mode 100644
index 0000000..f896e33
--- /dev/null
+++ b/prototypes/connections/module-list.h
@@ -0,0 +1,30 @@
+#ifndef PURE_MODULE_LIST_H
+#define PURE_MODULE_LIST_H
+#include "result.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Runtime-owned lists of records for the module executor. An item is one
+ * cell per field: a number, or text bytes the list owns. Every change made
+ * during an event is logged; a failed event undoes the log in reverse and a
+ * successful one frees what it replaced. */
+typedef struct { double num; unsigned char *text; size_t size; } PureCell;
+typedef struct PureList PureList;
+typedef struct { void *entries; size_t count, capacity; } PureUndo;
+
+enum { PURE_LIST_ITEMS = 65536 };
+
+PureList *pure_list_new(unsigned fields, const uint8_t *text_fields);
+void pure_list_free(PureList *);
+size_t pure_list_count(const PureList *);
+/* Insert a copy of cells (text bytes are copied) before position at. A NULL
+ * log means the change is not undone. */
+PureResult pure_list_insert(PureList *, size_t at, const PureCell *cells, PureUndo *);
+PureResult pure_list_remove(PureList *, size_t at, PureUndo *);
+PureResult pure_list_set(PureList *, size_t at, unsigned field, PureCell value, PureUndo *);
+PureCell pure_list_get(const PureList *, size_t at, unsigned field);
+void pure_undo_rollback(PureUndo *);
+void pure_undo_commit(PureUndo *);
+void pure_undo_free(PureUndo *);
+#endif
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c
index 950bb3e..c07b1e8 100644
--- a/prototypes/connections/module.c
+++ b/prototypes/connections/module.c
@@ -6,13 +6,15 @@
#include <string.h>
enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 8,
- CONSTANT_BYTES = 8, VIEW_BYTES = 16, HANDLER_BYTES = 12, OP_BYTES = 16 };
-enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5 /* type of the named field */ };
+ CONSTANT_BYTES = 8, RECORD_BYTES = 8, RECORD_FIELD_BYTES = 16, VIEW_BYTES = 16,
+ HANDLER_BYTES = 12, OP_BYTES = 16 };
+/* T_FIELD and T_ITEM stand for the type of the message or record field named. */
+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 { READ = IN_VIEW | IN_HANDLER };
-static const unsigned char magic[8] = {'P','U','R','E','M','O','D','3'};
+static const unsigned char magic[8] = {'P','U','R','E','M','O','D','4'};
/* 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] = {
@@ -66,6 +68,19 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP
[PURE_OP_TEXT_FROM] = {1, {T_BYTES}, T_TEXT, READ},
[PURE_OP_BYTES_FROM] = {1, {T_TEXT}, T_BYTES, READ},
[PURE_OP_CUR_COUNT] = {1, {T_CURSOR}, T_NUM, READ},
+ /* Record fields pop as many values as the list's record has fields. */
+ [PURE_OP_LIST_COUNT] = {0, {0}, T_NUM, READ},
+ [PURE_OP_LIST_APPEND] = {0, {0}, 0, IN_HANDLER},
+ [PURE_OP_LIST_INSERT] = {1, {T_INDEX}, 0, IN_HANDLER},
+ [PURE_OP_LIST_REMOVE] = {1, {T_INDEX}, 0, IN_HANDLER},
+ [PURE_OP_ITEM_GET] = {1, {T_INDEX}, T_ITEM, READ},
+ [PURE_OP_ITEM_SET] = {1, {T_INDEX}, 0, IN_HANDLER},
+ [PURE_OP_INDEX_LOAD] = {0, {0}, T_INDEX, READ},
+ [PURE_OP_INDEX_STORE] = {1, {T_INDEX}, 0, IN_HANDLER},
+ [PURE_OP_INDEX_MOVE] = {2, {T_INDEX, T_NUM}, T_INDEX, READ},
+ [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},
};
static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; }
@@ -133,7 +148,10 @@ static bool uses_arg(uint8_t op) {
case PURE_OP_DOC: case PURE_OP_DOC_SIZE: case PURE_OP_REPLACE: case PURE_OP_TEXT_CONST:
case PURE_OP_TEXT_LOAD: case PURE_OP_CUR_LOAD: case PURE_OP_CUR_START: case PURE_OP_CUR_END:
case PURE_OP_CUR_MOVE: case PURE_OP_CUR_STORE: case PURE_OP_TEXT_INSERT: case PURE_OP_TEXT_REMOVE:
- case PURE_OP_TEXT_LINE: case PURE_OP_TEXT_SET: case PURE_OP_CUR_COUNT:
+ 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:
return true;
default: return false;
}
@@ -144,10 +162,19 @@ static bool state_of(const PureModule *m, uint32_t state, uint8_t type) {
return state < m->states && m->state_type[state] == type;
}
+/* The list an item operation names: its low 16 bits (the field is above). */
+static uint32_t list_of(uint32_t arg) { return arg & 0xFFFF; }
+static uint32_t item_field(uint32_t arg) { return arg >> 16; }
+
+static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t field) {
+ return m->record_field_text[m->record[m->state_ref[list]].first + field] ? T_TEXT : T_NUM;
+}
+
/* Types and heights are checked per instruction; a cursor's type records
- * which text it belongs to. 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. */
+ * 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,
char *error, size_t size, unsigned index) {
if (block.start > m->code_count || block.length > m->code_count - block.start)
@@ -192,13 +219,32 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_
break;
case PURE_OP_CUR_LOAD: case PURE_OP_CUR_STORE: known = state_of(m, arg, PURE_STATE_CURSOR); break;
case PURE_OP_CUR_MOVE: case PURE_OP_CUR_COUNT: known = arg <= PURE_UNIT_LINE; break;
+ case PURE_OP_LIST_COUNT: case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE:
+ case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST:
+ known = state_of(m, arg, PURE_STATE_LIST);
+ break;
+ case PURE_OP_ITEM_GET: case PURE_OP_ITEM_SET:
+ known = state_of(m, list_of(arg), PURE_STATE_LIST) &&
+ 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;
default: break;
}
if (!known) return reject(error, size, "operand names no such state, field, constant or unit", pc);
+ /* Operand types, bottom to top; record operations take their fields. */
+ unsigned char need[PURE_RECORD_FIELDS + 2];
unsigned pops = signature[op->op].pops;
+ memcpy(need, signature[op->op].in, sizeof(signature[op->op].in));
+ if (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT) {
+ const PureRecord *record = &m->record[m->state_ref[arg]];
+ for (unsigned f = 0; f < record->count; ++f) need[pops + f] = field_type(m, arg, f);
+ pops += record->count;
+ } else if (op->op == PURE_OP_ITEM_SET) {
+ need[pops++] = field_type(m, list_of(arg), item_field(arg));
+ }
if (height < (int)pops) return reject(error, size, "stack underflow", pc);
for (unsigned k = 0; k < pops; ++k)
- if (types[height - (int)pops + (int)k] != signature[op->op].in[k])
+ if (types[height - (int)pops + (int)k] != need[k])
return reject(error, size, "operand type", pc);
/* The text a cursor operand belongs to. */
int owner = pops && types[height - 1] == T_CURSOR ? owners[height - 1] :
@@ -211,13 +257,26 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_
default: break;
}
if (!same_text) return reject(error, size, "cursor of another text", pc);
+ /* The list an index operand belongs to; the index is the first operand. */
+ unsigned index_owner = need[0] == T_INDEX ? owners[height - (int)pops] : 0;
+ bool same_list = true;
+ switch (op->op) {
+ case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: same_list = index_owner == arg; break;
+ case PURE_OP_ITEM_GET: case PURE_OP_ITEM_SET: same_list = index_owner == list_of(arg); break;
+ case PURE_OP_INDEX_STORE: same_list = index_owner == m->state_ref[arg]; break;
+ default: break;
+ }
+ if (!same_list) return reject(error, size, "index of another list", pc);
height -= (int)pops;
unsigned char out = signature[op->op].out;
if (out == T_FIELD) out = m->field[shape->field_start + arg].type == PURE_FIELD_U32 ? T_NUM : T_BYTES;
+ if (out == T_ITEM) out = field_type(m, list_of(arg), item_field(arg));
if (out) {
if (height == PURE_MODULE_STACK) return reject(error, size, "stack overflow", pc);
- owners[height] = (unsigned char)(op->op == PURE_OP_CUR_LOAD ? m->state_ref[arg] :
- op->op == PURE_OP_CUR_START || op->op == PURE_OP_CUR_END ? arg :
+ 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_MOVE ? index_owner :
owner >= 0 ? (unsigned)owner : 0);
types[height++] = out;
}
@@ -242,7 +301,7 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
if (error_size) error[0] = 0;
memset(out, 0, sizeof(*out));
if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic)))
- return reject(error, error_size, "not a Pure module v3", 0), PURE_INVALID;
+ return reject(error, error_size, "not a Pure module v4", 0), PURE_INVALID;
out->states = (uint16_t)read16(p + 8);
out->inputs = (uint16_t)read16(p + 10);
out->views = (uint16_t)read16(p + 12);
@@ -252,17 +311,21 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
out->fields = (uint16_t)read16(p + 24);
out->collections = (uint16_t)read16(p + 26);
out->constants = (uint16_t)read16(p + 28);
+ out->records = (uint16_t)read16(p + 30);
uint32_t constant_bytes = read32(p + 32);
- if (read16(p + 30) || read32(p + 36)) return reject(error, error_size, "reserved header bytes", 30), PURE_INVALID;
+ out->record_fields = (uint16_t)read16(p + 36);
+ if (read16(p + 38)) return reject(error, error_size, "reserved header bytes", 38), PURE_INVALID;
if (out->states > PURE_MODULE_STATES || out->inputs > PURE_MODULE_INPUTS ||
out->views > PURE_MODULE_VIEWS || out->handlers > PURE_MODULE_HANDLERS ||
out->code_count > PURE_MODULE_CODE || strings > PURE_MODULE_STRINGS ||
out->fields > PURE_MODULE_FIELDS || out->collections > PURE_MODULE_COLLECTIONS ||
- out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES)
+ out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES ||
+ out->records > PURE_MODULE_RECORDS || out->record_fields > PURE_MODULE_RECORD_FIELDS)
return reject(error, error_size, "module exceeds a limit", 8), PURE_LIMIT;
size_t expected = PURE_MODULE_HEADER + (size_t)out->states * STATE_BYTES +
(size_t)out->collections * COLLECTION_BYTES + (size_t)out->inputs * INPUT_BYTES +
(size_t)out->fields * FIELD_BYTES + (size_t)out->constants * CONSTANT_BYTES +
+ (size_t)out->records * RECORD_BYTES + (size_t)out->record_fields * RECORD_FIELD_BYTES +
(size_t)out->views * VIEW_BYTES + (size_t)out->handlers * HANDLER_BYTES +
(size_t)out->code_count * OP_BYTES + strings + constant_bytes;
if (size != expected) return reject(error, error_size, "module size", (unsigned)expected), PURE_INVALID;
@@ -273,7 +336,7 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
out->state_type[i] = (uint8_t)read32(at + 4);
if (!name_at(text, strings, read32(at), true, out->state_names[i]))
return reject(error, error_size, "state name", i), PURE_INVALID;
- if (read32(at + 4) > PURE_STATE_CURSOR) return reject(error, error_size, "state type", i), PURE_INVALID;
+ if (read32(at + 4) > PURE_STATE_INDEX) return reject(error, error_size, "state type", i), PURE_INVALID;
if (out->state_type[i] == PURE_STATE_NUM) {
out->initial[i] = read_double(at + 8);
if (!isfinite(out->initial[i])) return reject(error, error_size, "non-finite initial value", i), PURE_INVALID;
@@ -343,9 +406,45 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
!pure_utf8_valid((PureSpan){out->constant_data + out->constant_offset[i], out->constant_length[i]}))
return reject(error, error_size, "text constant", i), PURE_INVALID;
}
+ /* Records: fields in order, numbered from first; a text default is a constant. */
+ const unsigned char *fields_at = at + (size_t)out->records * RECORD_BYTES;
+ uint32_t next_record_field = 0;
+ for (uint32_t i = 0; i < out->records; ++i, at += RECORD_BYTES) {
+ PureRecord *record = &out->record[i];
+ record->first = (uint16_t)read16(at + 4);
+ record->count = (uint16_t)read16(at + 6);
+ if (!name_at(text, strings, read32(at), true, out->record_names[i]) || record->first != next_record_field ||
+ !record->count || record->count > PURE_RECORD_FIELDS || record->count > out->record_fields - next_record_field)
+ return reject(error, error_size, "record", i), PURE_INVALID;
+ for (uint32_t f = record->first; f < record->first + record->count; ++f) {
+ const unsigned char *entry = fields_at + (size_t)f * RECORD_FIELD_BYTES;
+ uint32_t type = read32(entry + 4);
+ out->record_field_text[f] = type == 1;
+ if (!name_at(text, strings, read32(entry), true, out->record_field_names[f]) || type > 1)
+ return reject(error, error_size, "record field", f), PURE_INVALID;
+ for (uint32_t k = record->first; k < f; ++k)
+ if (!strcmp(out->record_field_names[k], out->record_field_names[f]))
+ return reject(error, error_size, "duplicate record field", f), PURE_INVALID;
+ if (type == 1) {
+ if (read32(entry + 12) || read32(entry + 8) >= out->constants)
+ return reject(error, error_size, "record field default", f), PURE_INVALID;
+ out->record_field_default[f] = read32(entry + 8);
+ } else {
+ out->record_field_default[f] = read_double(entry + 8);
+ if (!isfinite(out->record_field_default[f]))
+ return reject(error, error_size, "record field default", f), PURE_INVALID;
+ }
+ }
+ next_record_field += record->count;
+ }
+ if (next_record_field != out->record_fields)
+ return reject(error, error_size, "unused record fields", next_record_field), PURE_INVALID;
+ at = fields_at + (size_t)out->record_fields * RECORD_FIELD_BYTES;
for (uint32_t i = 0; i < out->states; ++i)
if ((out->state_type[i] == PURE_STATE_TEXT && out->state_ref[i] >= out->constants) ||
- (out->state_type[i] == PURE_STATE_CURSOR && !state_of(out, out->state_ref[i], PURE_STATE_TEXT)))
+ (out->state_type[i] == PURE_STATE_CURSOR && !state_of(out, out->state_ref[i], PURE_STATE_TEXT)) ||
+ (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;
for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) {
if (!name_at(text, strings, read32(at), true, out->view_names[i]))
@@ -365,10 +464,12 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
if (at[1] || at[2] || at[3]) return reject(error, error_size, "instruction padding", i), PURE_INVALID;
out->code[i] = (PureOp){.op = at[0], .arg = read32(at + 4), .value = read_double(at + 8)};
}
- /* States, collections, inputs and views share one namespace. */
- const char *names[PURE_MODULE_STATES + PURE_MODULE_COLLECTIONS + PURE_MODULE_INPUTS + PURE_MODULE_VIEWS];
+ /* States, records, collections, inputs and views share one namespace. */
+ const char *names[PURE_MODULE_STATES + PURE_MODULE_RECORDS + PURE_MODULE_COLLECTIONS +
+ PURE_MODULE_INPUTS + PURE_MODULE_VIEWS];
size_t count = 0;
for (uint32_t i = 0; i < out->states; ++i) names[count++] = out->state_names[i];
+ 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];
@@ -508,6 +609,26 @@ static void move_cursors(const PureModule *m, double *values, uint32_t text, siz
}
}
+/* Indices follow their item across an insert or remove; an index whose item
+ * was removed names the next item (or the last, or 0 in an empty list). */
+static void move_indices(const PureModule *m, double *values, uint32_t list, size_t count,
+ size_t at, bool inserted) {
+ double last = count ? (double)(count - 1) : 0;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (m->state_type[i] != PURE_STATE_INDEX || m->state_ref[i] != list) continue;
+ if (inserted) {
+ if (count > 1 && values[i] >= (double)at) ++values[i];
+ } else {
+ if (values[i] > (double)at) --values[i];
+ values[i] = minimum(values[i], last);
+ }
+ }
+}
+
+static bool text_field(const PureModule *m, uint32_t list, uint32_t field) {
+ return m->record_field_text[m->record[m->state_ref[list]].first + field];
+}
+
/* Run a verified block. Types and stack bounds were proven at load. */
static PureResult run(const PureModule *m, double *values, PureBlock block, Event *ev, Slot *result) {
Slot stack[PURE_MODULE_STACK];
@@ -697,6 +818,105 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
continue;
}
case PURE_OP_BYTES_FROM: ++pc; continue; /* same bytes, now untyped */
+ case PURE_OP_LIST_COUNT: stack[sp++].num = (double)pure_list_count(app->lists[op->arg]); ++pc; continue;
+ case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: {
+ PureList *list = app->lists[op->arg];
+ const PureRecord *record = &m->record[m->state_ref[op->arg]];
+ size_t base = sp - record->count, count = pure_list_count(list), at = count;
+ sp = base;
+ if (op->op == PURE_OP_LIST_INSERT && (at = (size_t)stack[--sp].num) > count)
+ return fault(app, PURE_FAULT_NOT_FOUND, pc);
+ /* Room for every text first, so no span moves while the next is copied. */
+ size_t mark = ev->arena->used, total = 0;
+ for (unsigned f = 0; f < record->count; ++f)
+ if (text_field(m, op->arg, f)) total += (size_t)stack[base + f].bytes.length;
+ if (!reserve(ev->arena, total)) return PURE_ALLOCATION;
+ PureCell cells[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < record->count; ++f) {
+ PureSpan span = {0};
+ if (text_field(m, op->arg, f) &&
+ (status = contiguous(app, ev->arena, stack[base + f].bytes, &span)) != PURE_OK) return status;
+ cells[f] = (PureCell){stack[base + f].num, (unsigned char *)span.data, span.size};
+ }
+ status = pure_list_insert(list, at, cells, &app->undo);
+ ev->arena->used = mark;
+ if (status == PURE_LIMIT) return fault(app, PURE_FAULT_LIMIT, pc);
+ if (status != PURE_OK) return status;
+ move_indices(m, values, op->arg, pure_list_count(list), at, true);
+ ++pc;
+ continue;
+ }
+ case PURE_OP_LIST_REMOVE: {
+ size_t at = (size_t)stack[--sp].num;
+ status = pure_list_remove(app->lists[op->arg], at, &app->undo);
+ if (status == PURE_NOT_FOUND) return fault(app, PURE_FAULT_NOT_FOUND, pc);
+ if (status != PURE_OK) return status;
+ move_indices(m, values, op->arg, pure_list_count(app->lists[op->arg]), at, false);
+ ++pc;
+ continue;
+ }
+ case PURE_OP_ITEM_GET: {
+ uint32_t list = list_of(op->arg), field = item_field(op->arg);
+ size_t at = (size_t)stack[sp - 1].num;
+ if (at >= pure_list_count(app->lists[list])) return fault(app, PURE_FAULT_NOT_FOUND, pc);
+ PureCell cell = pure_list_get(app->lists[list], at, field);
+ if (!text_field(m, list, field)) stack[sp - 1].num = cell.num;
+ else {
+ /* A copy: the item may change or go before the value is used. */
+ if (!reserve(ev->arena, cell.size)) return PURE_ALLOCATION;
+ if (cell.size) memcpy(ev->arena->bytes + ev->arena->used, cell.text, cell.size);
+ stack[sp - 1].bytes = (PureBytes){FROM_ARENA, 0, {0, 0}, ev->arena->used, cell.size};
+ ev->arena->used += cell.size;
+ }
+ ++pc;
+ continue;
+ }
+ case PURE_OP_ITEM_SET: {
+ uint32_t list = list_of(op->arg), field = item_field(op->arg);
+ Slot value = stack[--sp];
+ size_t at = (size_t)stack[--sp].num, mark = ev->arena->used;
+ PureSpan span = {0};
+ if (text_field(m, list, field) && (status = contiguous(app, ev->arena, value.bytes, &span)) != PURE_OK)
+ return status;
+ status = pure_list_set(app->lists[list], at, field,
+ (PureCell){value.num, (unsigned char *)span.data, span.size}, &app->undo);
+ ev->arena->used = mark;
+ if (status == PURE_NOT_FOUND) return fault(app, PURE_FAULT_NOT_FOUND, pc);
+ if (status != PURE_OK) return status;
+ ++pc;
+ continue;
+ }
+ case PURE_OP_INDEX_LOAD:
+ stack[sp].num = values[op->arg];
+ stack[sp++].bytes.owner = m->state_ref[op->arg];
+ ++pc;
+ continue;
+ case PURE_OP_INDEX_STORE: {
+ /* An index from before a remove may be past the end. */
+ double position = stack[--sp].num;
+ if (position && position >= (double)pure_list_count(app->lists[m->state_ref[op->arg]]))
+ return fault(app, PURE_FAULT_NOT_FOUND, pc);
+ values[op->arg] = position;
+ ++pc;
+ continue;
+ }
+ case PURE_OP_INDEX_MOVE: {
+ double count = stack[--sp].num;
+ if (count != floor(count)) return fault(app, PURE_FAULT_LIMIT, pc);
+ size_t items = pure_list_count(app->lists[stack[sp - 1].bytes.owner]);
+ double last = items ? (double)(items - 1) : 0;
+ stack[sp - 1].num = maximum(0, minimum(stack[sp - 1].num + count, last));
+ ++pc;
+ continue;
+ }
+ case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: {
+ size_t items = pure_list_count(app->lists[op->arg]);
+ stack[sp].num = op->op == PURE_OP_INDEX_LAST && items ? (double)(items - 1) : 0;
+ stack[sp++].bytes.owner = (uint16_t)op->arg;
+ ++pc;
+ continue;
+ }
+ case PURE_OP_INDEX_NUM: ++pc; continue; /* the position itself */
default: break;
}
b = stack[--sp].num;
@@ -737,13 +957,18 @@ static PureResult new_text(PureDraft **out, const PureModule *m, uint32_t consta
return result;
}
+static PureList *new_list(const PureModule *m, uint32_t record) {
+ return pure_list_new(m->record[record].count, m->record_field_text + m->record[record].first);
+}
+
PureResult pure_app_start(PureApp *app, const PureModule *module, PureData data) {
*app = (PureApp){.module = module, .data = data, .fault_pc = UINT32_MAX};
memcpy(app->values, module->initial, sizeof(double) * module->states);
for (uint32_t i = 0; i < module->states; ++i) {
- if (module->state_type[i] == PURE_STATE_CURSOR) app->values[i] = 0;
- if (module->state_type[i] != PURE_STATE_TEXT) continue;
- PureResult result = new_text(&app->texts[i], module, module->state_ref[i]);
+ PureResult result = PURE_OK;
+ if (module->state_type[i] == PURE_STATE_TEXT) result = new_text(&app->texts[i], module, module->state_ref[i]);
+ if (module->state_type[i] == PURE_STATE_LIST && !(app->lists[i] = new_list(module, module->state_ref[i])))
+ result = PURE_ALLOCATION;
if (result != PURE_OK) { pure_app_stop(app); return result; }
}
return PURE_OK;
@@ -761,7 +986,11 @@ void pure_app_finish(PureApp *app) {
void pure_app_stop(PureApp *app) {
pure_app_finish(app);
- for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) pure_draft_close(app->texts[i]);
+ for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
+ pure_draft_close(app->texts[i]);
+ pure_list_free(app->lists[i]);
+ }
+ pure_undo_free(&app->undo);
free(app->arena.bytes);
free(app->view_arena.bytes);
memset(app, 0, sizeof(*app));
@@ -779,12 +1008,16 @@ static void hold_texts(PureApp *app, Texts *texts) {
}
}
-static void release_texts(PureApp *app, const Texts *texts, bool keep) {
+/* Texts return to their versions and lists undo their log, or both keep
+ * what the event did. */
+static void settle(PureApp *app, const Texts *texts, bool keep) {
for (uint32_t i = 0; i < app->module->states; ++i)
if (app->texts[i]) {
if (!keep) pure_draft_restore(app->texts[i], texts->before[i]);
pure_draft_hold(app->texts[i], false);
}
+ if (keep) pure_undo_commit(&app->undo);
+ else pure_undo_rollback(&app->undo);
}
/* All handlers of one event run in module order against pending states. */
@@ -806,7 +1039,7 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
Texts texts;
hold_texts(app, &texts);
PureResult result = run_handlers(app, event, ev, pending);
- release_texts(app, &texts, result == PURE_OK);
+ settle(app, &texts, result == PURE_OK);
if (result == PURE_OK) {
memcpy(app->values, pending, sizeof(pending));
++app->events;
@@ -885,8 +1118,8 @@ PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64
result = keep_reply(app);
if (result == PURE_OK && ev.modified)
result = app->data.commit(app->data.context, app->document, &app->revision);
- /* States and texts change only with a successful commit (or no document edit). */
- release_texts(app, &texts, result == PURE_OK);
+ /* States, texts and lists change only with a successful commit (or no document edit). */
+ settle(app, &texts, result == PURE_OK);
if (result == PURE_OK) {
memcpy(app->values, pending, sizeof(pending));
++app->events;
@@ -922,11 +1155,45 @@ PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *,
return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result;
}
+/* A kept list keeps its items. When its record changed, each field takes the
+ * old field of the same name and kind, or its default. */
+static PureResult keep_list(PureList **out, PureList *from, const PureModule *old, uint32_t old_record,
+ const PureModule *m, uint32_t record) {
+ const PureRecord *r = &m->record[record], *o = &old->record[old_record];
+ int source[PURE_RECORD_FIELDS];
+ bool same = r->count == o->count;
+ for (unsigned f = 0; f < r->count; ++f) {
+ source[f] = -1;
+ for (unsigned k = 0; k < o->count; ++k)
+ if (!strcmp(m->record_field_names[r->first + f], old->record_field_names[o->first + k]) &&
+ m->record_field_text[r->first + f] == old->record_field_text[o->first + k]) source[f] = (int)k;
+ same = same && source[f] == (int)f;
+ }
+ if (same) { *out = from; return PURE_OK; }
+ if (!(*out = new_list(m, record))) return PURE_ALLOCATION;
+ PureResult result = PURE_OK;
+ for (size_t at = 0; result == PURE_OK && at < pure_list_count(from); ++at) {
+ PureCell cells[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint32_t field = r->first + f, constant = (uint32_t)m->record_field_default[field];
+ cells[f] = source[f] >= 0 ? pure_list_get(from, at, (unsigned)source[f]) :
+ m->record_field_text[field] ?
+ (PureCell){0, (unsigned char *)m->constant_data + m->constant_offset[constant],
+ m->constant_length[constant]} :
+ (PureCell){m->record_field_default[field], NULL, 0};
+ }
+ result = pure_list_insert(*out, at, cells, NULL);
+ }
+ if (result != PURE_OK) { pure_list_free(*out); *out = NULL; }
+ return result;
+}
+
PureResult pure_app_replace(PureApp *app, const PureModule *module) {
pure_app_finish(app);
const PureModule *old = app->module;
double values[PURE_MODULE_STATES] = {0};
PureDraft *texts[PURE_MODULE_STATES] = {0};
+ PureList *lists[PURE_MODULE_STATES] = {0};
int kept[PURE_MODULE_STATES];
PureResult result = PURE_OK;
for (uint32_t i = 0; i < module->states; ++i) {
@@ -935,27 +1202,41 @@ PureResult pure_app_replace(PureApp *app, const PureModule *module) {
if (module->state_type[i] == PURE_STATE_TEXT) {
if (kept[i] >= 0) texts[i] = app->texts[kept[i]];
else if (result == PURE_OK) result = new_text(&texts[i], module, module->state_ref[i]);
+ } else if (module->state_type[i] == PURE_STATE_LIST) {
+ if (result != PURE_OK) continue;
+ if (kept[i] >= 0)
+ result = keep_list(&lists[i], app->lists[kept[i]], old, old->state_ref[kept[i]], module,
+ module->state_ref[i]);
+ else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION;
} else values[i] = kept[i] >= 0 ? app->values[kept[i]] : module->initial[i];
}
- /* A cursor keeps its position only while its text is the same kept text. */
+ /* A cursor or index keeps its position only while its text or list is the
+ * one it belonged to. */
for (uint32_t i = 0; i < module->states; ++i) {
- if (module->state_type[i] != PURE_STATE_CURSOR) continue;
- uint32_t text = module->state_ref[i];
- bool same = kept[i] >= 0 && kept[text] >= 0 && old->state_ref[kept[i]] == (unsigned)kept[text];
+ if (module->state_type[i] != PURE_STATE_CURSOR && module->state_type[i] != PURE_STATE_INDEX) continue;
+ uint32_t owner = module->state_ref[i];
+ bool same = kept[i] >= 0 && kept[owner] >= 0 && old->state_ref[kept[i]] == (unsigned)kept[owner];
values[i] = same ? app->values[kept[i]] : 0;
}
if (result != PURE_OK) {
- for (uint32_t i = 0; i < module->states; ++i)
+ for (uint32_t i = 0; i < module->states; ++i) {
if (texts[i] && kept[i] < 0) pure_draft_close(texts[i]);
+ if (lists[i] && (kept[i] < 0 || lists[i] != app->lists[kept[i]])) pure_list_free(lists[i]);
+ }
return result;
}
for (uint32_t i = 0; i < old->states; ++i) {
bool reused = false;
- for (uint32_t k = 0; k < module->states; ++k) reused = reused || (texts[k] && texts[k] == app->texts[i]);
- if (app->texts[i] && !reused) pure_draft_close(app->texts[i]);
+ for (uint32_t k = 0; k < module->states; ++k)
+ reused = reused || (texts[k] && texts[k] == app->texts[i]) || (lists[k] && lists[k] == app->lists[i]);
+ if (!reused) {
+ pure_draft_close(app->texts[i]);
+ pure_list_free(app->lists[i]);
+ }
}
memcpy(app->values, values, sizeof(values));
memcpy(app->texts, texts, sizeof(texts));
+ memcpy(app->lists, lists, sizeof(lists));
app->module = module;
return PURE_OK;
}
diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h
index c728fa8..6774cca 100644
--- a/prototypes/connections/module.h
+++ b/prototypes/connections/module.h
@@ -2,21 +2,24 @@
#define PURE_MODULE_H
#include "flow.h"
#include "result.h"
+#include "module-list.h"
#include "state.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
-/* Pure module format v3 (docs/design/008-module-format.md). A toolchain such
+/* Pure module format v4 (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 belongs to), stack heights,
- * forward-only branches, names, constants and effects. */
+ * operand types (including which text a cursor and which list an index
+ * belongs to), stack heights, forward-only branches, names, records,
+ * constants and effects. */
enum {
PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 24,
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_CONSTANT_BYTES = 8192, PURE_MODULE_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64,
+ PURE_RECORD_FIELDS = 16
};
typedef enum {
@@ -31,12 +34,17 @@ typedef enum {
PURE_OP_CUR_LOAD, PURE_OP_CUR_START, PURE_OP_CUR_END, PURE_OP_CUR_MOVE,
PURE_OP_CUR_STORE, PURE_OP_TEXT_INSERT, PURE_OP_TEXT_REMOVE, PURE_OP_TEXT_LINE,
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_COUNT
} PureOpcode;
typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3 } PureInputKind;
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 } PureStateType;
+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;
typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge;
@@ -56,8 +64,11 @@ typedef struct {
uint32_t prefix; /* fixed payload bytes before an optional bytes tail */
} PureInputShape;
+/* A record type: fields numbered from first, each a number or text. */
+typedef struct { uint16_t first, count; } PureRecord;
+
typedef struct {
- uint16_t states, inputs, views, handlers, fields, collections, constants;
+ uint16_t states, inputs, views, handlers, fields, collections, constants, records, record_fields;
uint32_t code_count;
char state_names[PURE_MODULE_STATES][PURE_MODULE_NAME];
char input_names[PURE_MODULE_INPUTS][PURE_MODULE_NAME];
@@ -65,7 +76,7 @@ typedef struct {
char collection_names[PURE_MODULE_COLLECTIONS][PURE_MODULE_NAME];
char input_keys[PURE_MODULE_INPUTS][PURE_MODULE_NAME];
uint8_t state_type[PURE_MODULE_STATES];
- uint16_t state_ref[PURE_MODULE_STATES]; /* text: initial constant; cursor: its text */
+ uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */
PureInputShape input[PURE_MODULE_INPUTS];
PureField field[PURE_MODULE_FIELDS];
double initial[PURE_MODULE_STATES];
@@ -74,6 +85,11 @@ typedef struct {
PureHandler handler[PURE_MODULE_HANDLERS];
PureOp code[PURE_MODULE_CODE];
uint32_t constant_offset[PURE_MODULE_CONSTANTS], constant_length[PURE_MODULE_CONSTANTS];
+ char record_names[PURE_MODULE_RECORDS][PURE_MODULE_NAME];
+ PureRecord record[PURE_MODULE_RECORDS];
+ char record_field_names[PURE_MODULE_RECORD_FIELDS][PURE_MODULE_NAME];
+ uint8_t record_field_text[PURE_MODULE_RECORD_FIELDS];
+ double record_field_default[PURE_MODULE_RECORD_FIELDS]; /* number, or text constant index */
unsigned char constant_data[PURE_MODULE_CONSTANT_BYTES];
} PureModule;
@@ -108,12 +124,15 @@ typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena;
/* One app instance: its states and at most one open event. Text states are
* runtime-owned drafts; cursors are positions the runtime keeps on character
- * boundaries. Every event is atomic: on failure nothing changes. */
+ * boundaries; lists are runtime-owned and an index always names an item
+ * (or 0 in an empty list). Every event is atomic: on failure nothing changes. */
typedef struct {
const PureModule *module;
PureData data;
- double values[PURE_MODULE_STATES]; /* numbers, and cursor byte positions */
+ double values[PURE_MODULE_STATES]; /* numbers, cursor byte positions and list indices */
PureDraft *texts[PURE_MODULE_STATES];
+ PureList *lists[PURE_MODULE_STATES];
+ PureUndo undo; /* list changes of the current event */
PureFault fault;
uint32_t fault_pc;
uint64_t ticks, events;
diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c
index e14e6ad..7f9f316 100644
--- a/prototypes/connections/pure-run.c
+++ b/prototypes/connections/pure-run.c
@@ -7,7 +7,8 @@
/* Test host for a verified module: reads commands from stdin and prints one
* 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. */
+ * 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. */
static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp", "limit", "not_found", "encoding"};
static bool load(const char *path, PureModule *module) {
@@ -36,6 +37,36 @@ static bool json_span(void *context, PureSpan span) {
return true;
}
+/* A text view is collected first: it may fail after some bytes. */
+typedef struct { unsigned char bytes[1 << 16]; size_t used; bool fits; } Collected;
+
+static bool collect(void *context, PureSpan span) {
+ Collected *c = context;
+ c->fits = c->fits && span.size <= sizeof(c->bytes) - c->used;
+ if (c->fits && span.size) memcpy(c->bytes + c->used, span.data, span.size);
+ if (c->fits) c->used += span.size;
+ return true;
+}
+
+static void show_list(const PureModule *m, const PureList *list, const PureRecord *record) {
+ putchar('[');
+ for (size_t at = 0; at < pure_list_count(list); ++at) {
+ printf("%s{", at ? "," : "");
+ for (unsigned f = 0; f < record->count; ++f) {
+ PureCell cell = pure_list_get(list, at, f);
+ printf("%s\"%s\":", f ? "," : "", m->record_field_names[record->first + f]);
+ if (!m->record_field_text[record->first + f]) printf("%.17g", cell.num);
+ else {
+ putchar('"');
+ json_span(NULL, (PureSpan){cell.text, cell.size});
+ putchar('"');
+ }
+ }
+ putchar('}');
+ }
+ putchar(']');
+}
+
static void show(PureApp *app, const char *failure) {
const PureModule *m = app->module;
printf("{\"states\":{");
@@ -45,16 +76,23 @@ static void show(PureApp *app, const char *failure) {
putchar('"');
pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), json_span, NULL);
putchar('"');
- } else printf("%.17g", app->values[i]);
+ } else if (m->state_type[i] == PURE_STATE_LIST) show_list(m, app->lists[i], &m->record[m->state_ref[i]]);
+ else printf("%.17g", app->values[i]);
}
printf("},\"views\":{");
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) {
- putchar('"');
- if (pure_app_view_text(app, i, json_span, NULL) != PURE_OK) printf("\\u0000failed");
- putchar('"');
+ 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('"');
+ }
} else if (pure_app_view(app, i, &value)) printf("%.17g", value);
else printf("null");
}
diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c
index 89b65f7..eb61521 100644
--- a/prototypes/connections/test-module.c
+++ b/prototypes/connections/test-module.c
@@ -15,13 +15,19 @@ typedef struct { const char *name, *key; int kind; bool keyed; const F *fields;
typedef struct { const char *name; uint32_t start, length; } V;
typedef struct { uint32_t event, start, length; } H;
typedef struct { uint8_t op; uint32_t arg; double value; } O;
-/* Text (type 1, ref = constant) and cursor (type 2, ref = text state) states
- * follow the number states; text views follow the number views. */
+/* Text (type 1, ref = constant), cursor (type 2, ref = text state), list
+ * (type 3, ref = record) and index (type 4, ref = list state) states follow
+ * the number states; text views follow the number views. */
typedef struct { const char *name; uint32_t type, ref; } T;
+/* A record field: a number and its default, or text (text = 1) whose
+ * 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;
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;
} Spec;
static unsigned char *put32(unsigned char *p, uint32_t v) {
@@ -36,7 +42,7 @@ static unsigned char *put_double(unsigned char *p, double value) {
return put32(p + 4, (uint32_t)(bits >> 32));
}
-/* Test-side encoder for format v3; the toolchain has its own. */
+/* Test-side encoder for format v4; 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;
@@ -53,6 +59,12 @@ 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;
+ size_t record_names = names, record_fields = 0;
+ for (size_t i = 0; i < spec.nr; ++i) {
+ text[names++] = spec.r[i].name;
+ for (size_t f = 0; f < spec.r[i].nfields; ++f) text[names++] = spec.r[i].fields[f].name;
+ record_fields += spec.r[i].nfields;
+ }
for (size_t i = 0; i < names; ++i) {
size_t length = strlen(text[i]);
offset[i] = (uint32_t)used;
@@ -65,13 +77,14 @@ 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, "PUREMOD3", 8);
+ memcpy(p, "PUREMOD4", 8);
size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv;
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);
put32(p + 20, (uint32_t)used);
p[24] = (unsigned char)fields; p[26] = (unsigned char)spec.nc; p[28] = (unsigned char)spec.nk;
+ p[30] = (unsigned char)spec.nr; p[36] = (unsigned char)record_fields;
put32(p + 32, (uint32_t)blob_used);
p += 40;
size_t n = 0;
@@ -98,6 +111,21 @@ static size_t build(unsigned char *out, Spec spec) {
p = put32(p, spec.in[i].fields[f].type);
}
for (size_t i = 0, at = 0; i < spec.nk; ++i) { p = put32(p, (uint32_t)at); p = put32(p, (uint32_t)strlen(spec.k[i])); at += strlen(spec.k[i]); }
+ size_t record_field = record_names;
+ for (size_t i = 0, first = 0; i < spec.nr; ++i) {
+ p = put32(p, offset[record_field]);
+ record_field += 1 + spec.r[i].nfields;
+ *p++ = (unsigned char)first; *p++ = 0; *p++ = (unsigned char)spec.r[i].nfields; *p++ = 0;
+ first += spec.r[i].nfields;
+ }
+ for (size_t i = 0, n = record_names; i < spec.nr; ++i, ++n)
+ for (size_t f = 0; f < spec.r[i].nfields; ++f) {
+ const RF *field = &spec.r[i].fields[f];
+ p = put32(p, offset[++n]);
+ p = put32(p, field->text);
+ if (field->text) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); }
+ else p = put_double(p, field->initial);
+ }
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.nh; ++i) { p = put32(p, spec.h[i].event); p = put32(p, spec.h[i].start); p = put32(p, spec.h[i].length); }
@@ -121,7 +149,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})
+#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})
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,
@@ -176,7 +204,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}, &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}, &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);
@@ -252,28 +280,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}, PURE_INVALID},
- {"state name", {bad_name, 1, i1, 0, clash, 0, none, 0, load_a, 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}, 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}, PURE_INVALID},
- {"input key", {s1, 1, bad_key, 1, clash, 0, none, 0, load_a, 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}, PURE_INVALID},
- {"duplicate name", {s1, 1, i1, 0, clash, 1, none, 0, load_a, 1, 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}, 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}, PURE_INVALID},
- {"empty view", {s1, 1, i1, 0, v_empty, 1, none, 0, store, 3, 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}, PURE_INVALID},
- {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, back, 2, 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}, PURE_INVALID},
- {"stack underflow", {s1, 1, i1, 0, clash, 0, h_all, 1, under, 1, 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}, 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}, 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}, 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}, PURE_INVALID},
- {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h0, 1, noncanon, 3, 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}, PURE_INVALID},
- {"unknown opcode", {s1, 1, i1, 0, clash, 0, h_two, 1, bad_op, 2, 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}, 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}, 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},
};
for (size_t i = 0; i < LEN(cases); ++i) {
PureResult result = load(cases[i].spec, &module_a);
@@ -285,9 +313,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}, &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}, &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});
+ 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});
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';
@@ -499,7 +527,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};
+ Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, 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(); }
@@ -565,22 +593,22 @@ 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};
+ bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, 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};
+ bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, 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};
+ bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, 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};
+ bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
static const O branch_value[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 3, 0}, {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};
+ 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"));
}
@@ -698,7 +726,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, textual, 3, constants, LEN(constants), text_views, 3, NULL, 0};
text_spec = spec;
if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
PureApp app;
@@ -733,7 +761,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};
+ Spec next = {fresh, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, kept, 2, other, 1, text_views, 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);
@@ -744,7 +772,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};
+ 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};
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;
@@ -758,13 +786,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};
+ 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};
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};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, cursor_on_number, 1, constants, 1, 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};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text constant"));
}
@@ -831,6 +859,219 @@ static void text_mutations(void) {
printf("{\"text_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
}
+static Spec list_spec;
+
+enum { LC = PURE_OP_LIST_COUNT, LA = PURE_OP_LIST_APPEND, LI = PURE_OP_LIST_INSERT, LR = PURE_OP_LIST_REMOVE,
+ IG = PURE_OP_ITEM_GET, IS = PURE_OP_ITEM_SET, XL = PURE_OP_INDEX_LOAD, XS = PURE_OP_INDEX_STORE,
+ XM = PURE_OP_INDEX_MOVE, XF = PURE_OP_INDEX_FIRST, XE = PURE_OP_INDEX_LAST, XN = PURE_OP_INDEX_NUM,
+ NOT = PURE_OP_NOT, TITLE = 1, DONE = 1 | 1u << 16 };
+
+static const char *item_title(const PureApp *app, uint32_t list, size_t at) {
+ static char title[64];
+ PureCell cell = pure_list_get(app->lists[list], at, 0);
+ CHECK(cell.size < sizeof(title));
+ if (cell.size) memcpy(title, cell.text, cell.size);
+ title[cell.size] = 0;
+ return title;
+}
+
+/* A task list: made (num), tasks (list of task {title, done}), at (index of
+ * tasks). Indices follow their item across inserts and removes; a failed
+ * event undoes every list change. */
+static void lists(void) {
+ static const S nums[] = {{"made", 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[] = {"", "new", "x"};
+ static const I inputs[] = {
+ {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("ins", "KeyI"), KEY("del", "Delete"),
+ KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("toggle", "Space"), KEY("rename", "KeyR"), KEY("bad", "KeyB"),
+ };
+ static const O code[] = {
+ {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 0 typed */
+ {XL, 2, 0}, {TK, 1, 0}, {C, 0, 0}, {LI, 1, 0}, /* 9 ins */
+ {XL, 2, 0}, {LR, 1, 0}, /* 13 del */
+ {XL, 2, 0}, {C, 0, -1}, {XM, 0, 0}, {XS, 2, 0}, /* 15 up */
+ {XL, 2, 0}, {C, 0, 1}, {XM, 0, 0}, {XS, 2, 0}, /* 19 down */
+ {XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 23 toggle */
+ {XL, 2, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {TK, 2, 0}, {CAT, 0, 0}, {IS, TITLE, 0}, /* 28 rename */
+ {XL, 2, 0}, {TK, 2, 0}, {IS, TITLE, 0}, {XL, 2, 0}, {LR, 1, 0}, {TK, 1, 0}, {C, 0, 0}, /* 34 bad */
+ {LA, 1, 0}, {C, 0, 1}, {C, 0, 0}, {DIV, 0, 0}, {ST, 0, 0},
+ {LC, 1, 0}, /* 46 view count */
+ {XL, 2, 0}, {XN, 0, 0}, /* 47 view position */
+ {XL, 2, 0}, {IG, DONE, 0}, /* 49 view done */
+ {XL, 2, 0}, {IG, TITLE, 0}, /* 51 view current */
+ };
+ static const H handlers[] = {{1, 0, 9}, {3, 9, 4}, {5, 13, 2}, {7, 15, 4}, {9, 19, 4}, {11, 23, 5},
+ {13, 28, 6}, {15, 34, 12}};
+ 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};
+ list_spec = spec;
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ double value;
+ CHECK(number_view(&app, 0) == 0 && number_view(&app, 1) == 0 && !pure_app_view(&app, 2, &value));
+ CHECK(pure_app_view_text(&app, 3, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND); /* no item yet */
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"a", 1}) == PURE_OK);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"b", 1}) == PURE_OK);
+ CHECK(pure_list_count(app.lists[1]) == 2 && app.values[2] == 1 && app.values[0] == 2);
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0);
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); /* stops at the first */
+ /* Inserting before the current item: the index follows it. */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 1);
+ CHECK(!strcmp(item_title(&app, 1, 0), "new") && !strcmp(view_text(&app, 3), "a"));
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && number_view(&app, 2) == 1);
+ CHECK(pure_app_input(&app, 6, PURE_EDGE_DOWN) == PURE_OK && !strcmp(view_text(&app, 3), "ax"));
+ CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 2);
+ CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 2); /* stops at the last */
+ /* Removing the last item: the index moves to the new last. */
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 1 && pure_list_count(app.lists[1]) == 2);
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0);
+ /* Removing the current item: the index names the next. */
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0 && !strcmp(view_text(&app, 3), "ax"));
+ /* A failing event undoes a set, a remove and an append. */
+ CHECK(pure_app_input(&app, 7, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == PURE_FAULT_DIVIDE);
+ CHECK(pure_list_count(app.lists[1]) == 1 && !strcmp(item_title(&app, 1, 0), "ax") &&
+ pure_list_get(app.lists[1], 0, 1).num == 1 && app.values[2] == 0 && app.values[0] == 2);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.lists[1]) == 0 && app.values[2] == 0);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_NOT_FOUND && app.fault == PURE_FAULT_NOT_FOUND);
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_NOT_FOUND);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.lists[1]) == 1 && app.values[2] == 0);
+ for (int i = 0; i < 3; ++i) CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xea\xb0\x80", 3}) == PURE_OK);
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 2);
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK);
+
+ /* Replacement keeps the list by name. The same record keeps the list
+ * itself; a changed record keeps each field by name and kind and gives new
+ * 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};
+ 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);
+ static const RF changed_fields[] = {{"done", 0, 0}, {"note", 1, 1}, {"title", 0, 7}, {"rank", 0, 5}};
+ static const R changed[] = {{"task", changed_fields, 4}};
+ Spec next = same;
+ next.r = changed;
+ CHECK(load(next, &module_a) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_a) == PURE_OK && pure_list_count(app.lists[1]) == 4 && app.values[2] == 2);
+ PureCell cell = pure_list_get(app.lists[1], 2, 0);
+ CHECK(cell.num == 1); /* done kept by name */
+ cell = pure_list_get(app.lists[1], 2, 1);
+ CHECK(cell.size == 3 && !memcmp(cell.text, "new", 3)); /* note: its default */
+ CHECK(pure_list_get(app.lists[1], 2, 2).num == 7); /* title is now a number: default */
+ CHECK(pure_list_get(app.lists[1], 0, 3).num == 5);
+ /* A state whose kind changed starts again. */
+ static const T swapped_kinds[] = {{"tasks", 1, 0}, {"at", 3, 0}};
+ next.t = swapped_kinds;
+ CHECK(load(next, &module_b) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK && !app.lists[1] && pure_draft_size(app.texts[1]) == 0 &&
+ pure_list_count(app.lists[2]) == 0);
+ pure_app_stop(&app);
+
+ /* Verification of list operations. */
+ static const T two_lists[] = {{"a", 3, 0}, {"b", 3, 0}, {"ia", 4, 1}};
+ 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};
+ 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}};
+ bad.o = swapped; bad.no = 3; bad.h = frame3;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ static const O short_record[] = {{TK, 0, 0}, {LA, 1, 0}};
+ bad.o = short_record; bad.no = 2; bad.h = frame;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "stack underflow"));
+ static const O no_field[] = {{XL, 3, 0}, {IG, 1 | 2u << 16, 0}, {ST, 0, 0}};
+ bad.o = no_field; bad.no = 3; bad.h = frame3;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state"));
+ static const O index_as_number[] = {{XL, 3, 0}, {ST, 0, 0}};
+ bad.o = index_as_number; bad.no = 2; bad.h = frame;
+ 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};
+ 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};
+ 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;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference"));
+ bad.t = NULL; bad.nt = 0;
+ static const RF repeated[] = {{"x", 0, 0}, {"x", 1, 0}};
+ bad.r = (R[]){{"task", repeated, 2}};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate record field"));
+ static const RF far_default[] = {{"x", 1, 3}};
+ bad.r = (R[]){{"task", far_default, 1}};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record field default"));
+ static const RF infinite[] = {{"x", 0, INFINITY}};
+ bad.r = (R[]){{"task", infinite, 1}};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record field default"));
+ bad.r = (R[]){{"task", NULL, 0}};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record"));
+ bad.r = (R[]){{"made", task_fields, 2}};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate name"));
+}
+
+/* Damaged list modules either fail to load or keep every index on an item
+ * of its list and every text field valid UTF-8. */
+static void list_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, list_spec);
+ static const char *const words[] = {"a", "\xea\xb0\x80", "\n", ""};
+ uint32_t state = 0x85ebca6bu;
+ 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 < 24; ++e) {
+ uint32_t input = next_random(&state) % 8;
+ if (module_b.input[input].kind == PURE_KIND_TEXT) {
+ const char *word = words[next_random(&state) % LEN(words)];
+ pure_app_text(&app, input, (PureSpan){(const unsigned char *)word, strlen(word)});
+ } else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN);
+ else pure_app_frame(&app);
+ for (uint32_t v = 0; v < module_b.views; ++v) {
+ double value;
+ 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);
+ }
+ for (uint32_t s = 0; s < module_b.states; ++s) {
+ if (module_b.state_type[s] == PURE_STATE_INDEX) {
+ size_t count = pure_list_count(app.lists[module_b.state_ref[s]]);
+ CHECK(app.values[s] == floor(app.values[s]) && app.values[s] >= 0 &&
+ app.values[s] <= (count ? (double)(count - 1) : 0));
+ } else if (module_b.state_type[s] == PURE_STATE_LIST) {
+ const PureRecord *record = &module_b.record[module_b.state_ref[s]];
+ for (size_t at = 0; at < pure_list_count(app.lists[s]); ++at)
+ for (unsigned f = 0; f < record->count; ++f) {
+ PureCell cell = pure_list_get(app.lists[s], at, f);
+ if (module_b.record_field_text[record->first + f])
+ CHECK(pure_utf8_valid((PureSpan){cell.text, cell.size}));
+ else CHECK(isfinite(cell.num));
+ }
+ } else if (module_b.state_type[s] == PURE_STATE_NUM) CHECK(isfinite(app.values[s]));
+ }
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"list_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
int main(void) {
behaviour();
faults();
@@ -840,7 +1081,9 @@ int main(void) {
message_mutations();
texts();
text_mutations();
+ lists();
+ list_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\"}");
+ 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\"}");
return 0;
}