aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes
diff options
context:
space:
mode:
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;
}