aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/flow.c41
-rw-r--r--core/flow.h25
-rw-r--r--core/module-list.c428
-rw-r--r--core/module-list.h74
-rw-r--r--core/module-save.c488
-rw-r--r--core/module-save.h36
-rw-r--r--core/module-store.c88
-rw-r--r--core/module-store.h11
-rw-r--r--core/module-text.c189
-rw-r--r--core/module-text.h34
-rw-r--r--core/module.c2122
-rw-r--r--core/module.h343
-rw-r--r--core/result.h8
-rw-r--r--core/runtime.c394
-rw-r--r--core/runtime.h150
-rw-r--r--core/state.h84
-rw-r--r--core/store.h54
-rw-r--r--core/test-module.c2853
18 files changed, 7422 insertions, 0 deletions
diff --git a/core/flow.c b/core/flow.c
new file mode 100644
index 0000000..1070c70
--- /dev/null
+++ b/core/flow.c
@@ -0,0 +1,41 @@
+#include "flow.h"
+#include <assert.h>
+
+void pure_transfer_abort(PureTransfer *t) {
+ if (!t->active) return;
+ PureRelease release = t->release;
+ void *owner = t->owner;
+ *t = (PureTransfer){0};
+ if (release) release(owner);
+}
+
+static void advance(PureTransfer *t) {
+ while (t->active && t->part < 2 && t->offset == t->spans[t->part].size) {
+ t->part++;
+ t->offset = 0;
+ }
+ if (t->active && t->part == 2) pure_transfer_abort(t);
+}
+
+void pure_transfer_init(PureTransfer *t, PureSpan header, PureSpan body,
+ void *owner, PureRelease release) {
+ assert(!t->active);
+ assert((!header.size || header.data) && (!body.size || body.data));
+ *t = (PureTransfer){.spans = {header, body}, .owner = owner,
+ .release = release, .active = true};
+ advance(t);
+}
+
+PureSpan pure_transfer_next(const PureTransfer *t, size_t limit) {
+ if (!t->active) return (PureSpan){0};
+ PureSpan span = t->spans[t->part];
+ size_t remaining = span.size - t->offset;
+ return (PureSpan){span.data + t->offset, remaining < limit ? remaining : limit};
+}
+
+bool pure_transfer_consume(PureTransfer *t, size_t bytes) {
+ if (!t->active || bytes > t->spans[t->part].size - t->offset) return false;
+ t->offset += bytes;
+ advance(t);
+ return true;
+}
diff --git a/core/flow.h b/core/flow.h
new file mode 100644
index 0000000..6e27c58
--- /dev/null
+++ b/core/flow.h
@@ -0,0 +1,25 @@
+#ifndef PURE_FLOW_H
+#define PURE_FLOW_H
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef struct { const unsigned char *data; size_t size; } PureSpan;
+typedef void (*PureRelease)(void *owner);
+
+/* A borrowed body stays alive until every byte is consumed or the transfer is
+ * aborted. A successful send() consumes bytes; peer receipt is a separate fact.
+ * Single owner thread; callbacks must not reenter or reuse this transfer. */
+typedef struct {
+ PureSpan spans[2];
+ size_t part, offset;
+ void *owner;
+ PureRelease release;
+ bool active;
+} PureTransfer;
+
+void pure_transfer_init(PureTransfer *, PureSpan header, PureSpan body,
+ void *owner, PureRelease);
+PureSpan pure_transfer_next(const PureTransfer *, size_t limit);
+bool pure_transfer_consume(PureTransfer *, size_t bytes);
+void pure_transfer_abort(PureTransfer *);
+#endif
diff --git a/core/module-list.c b/core/module-list.c
new file mode 100644
index 0000000..2cd2b1c
--- /dev/null
+++ b/core/module-list.c
@@ -0,0 +1,428 @@
+#include "module-list.h"
+#include "module-text.h"
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct PureList {
+ unsigned fields;
+ uint8_t text_fields[16];
+ PureCell **items;
+ size_t count, capacity;
+ PureDraft *draft; /* the stored copy, or NULL */
+ size_t header; /* draft bytes before the first item */
+ uint64_t id, version; /* which list, and a number every change moves on */
+ uint64_t shares; /* copies made so far: an item made before the last one is shared */
+ PureRetired *retired; /* where shared items go when the list lets them go */
+ unsigned refs; /* a shared copy's references */
+ bool copy; /* a shared copy: it owns its item array, not its items */
+};
+
+typedef enum { INSERTED, REMOVED, CHANGED, REPLACED } Change;
+typedef struct {
+ Change change;
+ PureList *list;
+ size_t at;
+ unsigned field;
+ PureCell *item; /* REMOVED: the removed item; REPLACED: the shared item a copy replaced */
+ PureCell old; /* CHANGED: the replaced cell */
+} Entry;
+typedef struct { PureCell *item; uint16_t text; uint8_t fields; } Retired;
+
+static uint64_t lists_made;
+
+/* An item's cells follow the list's share count when the item was made. One
+ * made before the last share may be read by a shared copy, so it never
+ * changes again (a change copies it) and is retired instead of freed. */
+static PureCell *new_item(const PureList *list) {
+ uint64_t *made = calloc(1, sizeof(*made) + (list->fields ? list->fields : 1) * sizeof(PureCell));
+ if (!made) return NULL;
+ *made = list->shares;
+ return (PureCell *)(made + 1);
+}
+
+static bool shared(const PureList *list, const PureCell *item) { return ((const uint64_t *)item)[-1] < list->shares; }
+
+static void free_cells(PureCell *item, unsigned fields, unsigned text) {
+ for (unsigned f = 0; f < fields; ++f)
+ if (text >> f & 1) free(item[f].text);
+ free((uint64_t *)item - 1);
+}
+
+static unsigned text_mask(const PureList *list) {
+ unsigned text = 0;
+ for (unsigned f = 0; f < list->fields; ++f) text |= (unsigned)(list->text_fields[f] != 0) << f;
+ return text;
+}
+
+static void free_item(const PureList *list, PureCell *item) {
+ if (item) free_cells(item, list->fields, text_mask(list));
+}
+
+/* The list no longer has item: free it, or retire it while a copy may read it. */
+static void let_go(PureList *list, PureCell *item) {
+ if (!shared(list, item)) { free_item(list, item); return; }
+ PureRetired *retired = list->retired;
+ if (retired->count == retired->capacity) {
+ size_t capacity = retired->capacity ? 2 * retired->capacity : 16;
+ Retired *grown = realloc(retired->entries, capacity * sizeof(*grown));
+ if (!grown) return; /* kept for good rather than freed while read */
+ retired->entries = grown;
+ retired->capacity = capacity;
+ }
+ ((Retired *)retired->entries)[retired->count++] = (Retired){item, (uint16_t)text_mask(list), (uint8_t)list->fields};
+}
+
+void pure_retired_free(PureRetired *retired) {
+ Retired *entries = retired->entries;
+ for (size_t i = 0; i < retired->count; ++i) free_cells(entries[i].item, entries[i].fields, entries[i].text);
+ free(entries);
+ *retired = (PureRetired){0};
+}
+
+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);
+ list->id = __atomic_add_fetch(&lists_made, 1, __ATOMIC_RELAXED);
+ return list;
+}
+
+void pure_list_free(PureList *list) {
+ if (!list) return;
+ if (list->copy) {
+ if (__atomic_sub_fetch(&list->refs, 1, __ATOMIC_ACQ_REL)) return;
+ } else
+ for (size_t i = 0; i < list->count; ++i) let_go(list, list->items[i]);
+ free(list->items);
+ free(list);
+}
+
+size_t pure_list_count(const PureList *list) { return list->count; }
+
+PureList *pure_list_share(PureList *list, PureRetired *retired) {
+ PureList *copy = malloc(sizeof(*copy));
+ PureCell **items = list->count ? malloc(list->count * sizeof(*items)) : NULL;
+ if (!copy || (list->count && !items)) {
+ free(copy);
+ free(items);
+ return NULL;
+ }
+ if (list->count) memcpy(items, list->items, list->count * sizeof(*items));
+ *copy = (PureList){.fields = list->fields, .items = items, .count = list->count, .capacity = list->count,
+ .id = list->id, .version = list->version, .refs = 1, .copy = true};
+ memcpy(copy->text_fields, list->text_fields, sizeof(copy->text_fields));
+ list->retired = retired;
+ ++list->shares;
+ return copy;
+}
+
+void pure_list_hold(PureList *copy) { __atomic_add_fetch(&copy->refs, 1, __ATOMIC_RELAXED); }
+
+bool pure_list_current(const PureList *copy, const PureList *list) {
+ return copy->id == list->id && copy->version == list->version;
+}
+
+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;
+}
+
+/* ---- The stored form: each field in order, a number as its binary64 bits
+ * (little-endian), a text as a u32le length and its bytes. */
+
+static size_t cell_bytes(const PureList *list, unsigned field, PureCell cell) {
+ return list->text_fields[field] ? 4 + cell.size : 8;
+}
+
+static size_t item_bytes(const PureList *list, const PureCell *item) {
+ size_t size = 0;
+ for (unsigned f = 0; f < list->fields; ++f) size += cell_bytes(list, f, item[f]);
+ return size;
+}
+
+/* Where an item starts in the draft. Linear in the items before it. */
+static size_t item_offset(const PureList *list, size_t at) {
+ size_t offset = list->header;
+ for (size_t i = 0; i < at; ++i) offset += item_bytes(list, list->items[i]);
+ return offset;
+}
+
+static unsigned char *encode_cell(const PureList *list, unsigned field, PureCell cell, unsigned char *out) {
+ uint64_t bits = cell.size;
+ unsigned width = 4;
+ if (!list->text_fields[field]) { memcpy(&bits, &cell.num, sizeof(bits)); width = 8; }
+ for (unsigned i = 0; i < width; ++i) *out++ = (unsigned char)(bits >> (8 * i));
+ if (list->text_fields[field] && cell.size) { memcpy(out, cell.text, cell.size); out += cell.size; }
+ return out;
+}
+
+/* Replace remove bytes of the draft, within bytes into the item at `at` (or
+ * after the last item), with bytes; nothing to do without a draft. */
+static PureResult mirror(PureList *list, size_t at, size_t within, size_t remove, const unsigned char *bytes,
+ size_t size) {
+ if (!list->draft) return PURE_OK;
+ return pure_draft_splice(list->draft, item_offset(list, at) + within, remove, (PureSpan){bytes, size});
+}
+
+void pure_list_mirror(PureList *list, PureDraft *draft, size_t header) {
+ list->draft = draft;
+ list->header = header;
+}
+
+PURE_COLD PureResult pure_list_write(PureList *list) {
+ size_t size = 0;
+ for (size_t i = 0; i < list->count; ++i) size += item_bytes(list, list->items[i]);
+ unsigned char *bytes = size ? malloc(size) : NULL, *at = bytes;
+ if (size && !bytes) return PURE_ALLOCATION;
+ for (size_t i = 0; i < list->count; ++i)
+ for (unsigned f = 0; f < list->fields; ++f) at = encode_cell(list, f, list->items[i][f], at);
+ PureResult result = mirror(list, 0, 0, 0, bytes, size);
+ free(bytes);
+ return result;
+}
+
+PURE_COLD PureResult pure_list_read(PureList *list, const unsigned char *bytes, size_t size) {
+ PureCell cells[16];
+ for (size_t at = 0; at < size;) {
+ for (unsigned f = 0; f < list->fields; ++f) {
+ size_t width = list->text_fields[f] ? 4 : 8;
+ if (size - at < width) return PURE_STORE_ERROR;
+ uint64_t bits = 0;
+ for (size_t i = 0; i < width; ++i) bits |= (uint64_t)bytes[at + i] << (8 * i);
+ at += width;
+ if (!list->text_fields[f]) {
+ cells[f] = (PureCell){0};
+ memcpy(&cells[f].num, &bits, sizeof(bits));
+ if (!isfinite(cells[f].num)) return PURE_STORE_ERROR;
+ continue;
+ }
+ if (bits > size - at || !pure_utf8_valid((PureSpan){bytes + at, (size_t)bits}))
+ return PURE_STORE_ERROR;
+ cells[f] = (PureCell){0, (unsigned char *)bytes + at, (size_t)bits};
+ at += (size_t)bits;
+ }
+ PureResult result = pure_list_insert(list, list->count, cells, NULL);
+ if (result != PURE_OK) return result;
+ }
+ return PURE_OK;
+}
+
+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;
+ ++list->version;
+}
+
+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;
+ ++list->version;
+ 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 = new_item(list);
+ 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; }
+ size_t size = list->draft ? item_bytes(list, item) : 0;
+ unsigned char *bytes = size ? malloc(size) : NULL, *end = bytes;
+ if (size && !bytes) { free_item(list, item); return PURE_ALLOCATION; }
+ for (unsigned f = 0; bytes && f < list->fields; ++f) end = encode_cell(list, f, item[f], end);
+ if (undo && !log_change(undo, (Entry){.change = INSERTED, .list = list, .at = at})) {
+ free(bytes);
+ free_item(list, item);
+ return PURE_ALLOCATION;
+ }
+ PureResult result = mirror(list, at, 0, 0, bytes, size);
+ free(bytes);
+ if (result != PURE_OK) {
+ if (undo) --undo->count;
+ free_item(list, item);
+ return result;
+ }
+ 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;
+ PureResult result = mirror(list, at, 0, item_bytes(list, list->items[at]), NULL, 0);
+ if (result != PURE_OK) { --undo->count; return result; }
+ take(list, at);
+ return PURE_OK;
+}
+
+/* A shared item's copy with every field but one (left empty). */
+static PureCell *copy_item(const PureList *list, const PureCell *item, unsigned except) {
+ PureCell *fresh = new_item(list);
+ for (unsigned f = 0; fresh && f < list->fields; ++f)
+ if (f != except && !copy_cell(list, f, item[f], &fresh[f])) {
+ free_item(list, fresh);
+ fresh = NULL;
+ }
+ return fresh;
+}
+
+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, *item = list->items[at];
+ if (!copy_cell(list, field, value, &copy)) return PURE_ALLOCATION;
+ PureCell *fresh = shared(list, item) ? copy_item(list, item, field) : item;
+ size_t size = list->draft ? cell_bytes(list, field, copy) : 0;
+ unsigned char *bytes = size ? malloc(size) : NULL;
+ Entry entry = fresh == item ? (Entry){.change = CHANGED, .list = list, .at = at, .field = field, .old = item[field]} :
+ (Entry){.change = REPLACED, .list = list, .at = at, .item = item};
+ if (!fresh || (size && !bytes) || !log_change(undo, entry)) {
+ free(bytes);
+ free(copy.text);
+ if (fresh != item) free_item(list, fresh);
+ return PURE_ALLOCATION;
+ }
+ if (bytes) encode_cell(list, field, copy, bytes);
+ size_t within = 0;
+ for (unsigned f = 0; f < field; ++f) within += cell_bytes(list, f, item[f]);
+ PureResult result = mirror(list, at, within, cell_bytes(list, field, item[field]), bytes, size);
+ free(bytes);
+ if (result != PURE_OK) {
+ --undo->count;
+ free(copy.text);
+ if (fresh != item) free_item(list, fresh);
+ return result;
+ }
+ fresh[field] = copy;
+ list->items[at] = fresh;
+ ++list->version;
+ return PURE_OK;
+}
+
+PureCell pure_list_get(const PureList *list, size_t at, unsigned field) {
+ return list->items[at][field];
+}
+
+/* How two cells of a field order: numbers by value (-0 and +0 are one key),
+ * texts by their bytes. */
+static int order(const PureList *list, unsigned field, PureCell a, PureCell b) {
+ if (!list->text_fields[field]) return (a.num > b.num) - (a.num < b.num);
+ size_t shorter = a.size < b.size ? a.size : b.size;
+ int bytes = shorter ? memcmp(a.text, b.text, shorter) : 0;
+ return bytes ? (bytes > 0) - (bytes < 0) : (a.size > b.size) - (a.size < b.size);
+}
+
+size_t pure_list_seek(const PureList *list, unsigned field, PureCell key, bool *found) {
+ size_t low = 0, high = list->count;
+ while (low < high) {
+ size_t middle = low + (high - low) / 2;
+ if (order(list, field, list->items[middle][field], key) < 0) low = middle + 1;
+ else high = middle;
+ }
+ *found = low < list->count && !order(list, field, list->items[low][field], key);
+ return low;
+}
+
+/* Positions sorted by their items' field, stably (spare holds count). */
+PURE_COLD static void merge_sort(const PureList *list, unsigned field, size_t *at, size_t *spare, size_t count) {
+ if (count < 2) return;
+ size_t half = count / 2, i = 0, j = half, k = 0;
+ merge_sort(list, field, at, spare, half);
+ merge_sort(list, field, at + half, spare, count - half);
+ while (i < half && j < count)
+ spare[k++] = order(list, field, list->items[at[j]][field], list->items[at[i]][field]) < 0 ? at[j++] : at[i++];
+ while (i < half) spare[k++] = at[i++];
+ while (j < count) spare[k++] = at[j++];
+ memcpy(at, spare, count * sizeof(*at));
+}
+
+PURE_COLD PureResult pure_list_sorted(const PureList *list, unsigned field, PureList **out, size_t *moved) {
+ *out = NULL;
+ bool ordered = true;
+ for (size_t i = 1; i < list->count && ordered; ++i)
+ ordered = order(list, field, list->items[i - 1][field], list->items[i][field]) < 0;
+ if (ordered) return PURE_OK;
+ size_t *at = malloc(2 * list->count * sizeof(*at));
+ if (!at) return PURE_ALLOCATION;
+ for (size_t i = 0; i < list->count; ++i) at[i] = i;
+ merge_sort(list, field, at, at + list->count, list->count);
+ PureResult result = PURE_OK;
+ for (size_t i = 1; i < list->count && result == PURE_OK; ++i)
+ if (!order(list, field, list->items[at[i - 1]][field], list->items[at[i]][field])) result = PURE_CONFLICT;
+ PureList *sorted = result == PURE_OK ? pure_list_new(list->fields, list->text_fields) : NULL;
+ if (result == PURE_OK && !sorted) result = PURE_ALLOCATION;
+ for (size_t i = 0; result == PURE_OK && i < list->count; ++i) {
+ result = pure_list_insert(sorted, i, list->items[at[i]], NULL);
+ moved[at[i]] = i;
+ }
+ free(at);
+ if (result != PURE_OK) {
+ pure_list_free(sorted);
+ return result;
+ }
+ *out = sorted;
+ return PURE_OK;
+}
+
+void pure_undo_rollback(PureUndo *undo, size_t mark) {
+ Entry *entries = undo->entries;
+ for (size_t i = undo->count; i-- > mark;) {
+ 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->change == REPLACED) {
+ free_item(e->list, e->list->items[e->at]);
+ e->list->items[e->at] = e->item;
+ ++e->list->version;
+ } 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;
+ ++e->list->version;
+ }
+ }
+ undo->count = mark;
+}
+
+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 || e->change == REPLACED) let_go(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/core/module-list.h b/core/module-list.h
new file mode 100644
index 0000000..a721ff0
--- /dev/null
+++ b/core/module-list.h
@@ -0,0 +1,74 @@
+#ifndef PURE_MODULE_LIST_H
+#define PURE_MODULE_LIST_H
+#include "result.h"
+#include "state.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Executor paths that run once per module or app (loading, verifying,
+ * starting, replacing, reading stored states) are built for size; events
+ * stay built for speed. */
+#if defined(__GNUC__) || defined(__clang__)
+#define PURE_COLD __attribute__((cold))
+#else
+#define PURE_COLD
+#endif
+
+/* 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;
+/* Items a list let go while a shared copy may still read them. */
+typedef struct { void *entries; size_t count, capacity; } PureRetired;
+
+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 *);
+/* Share the items as they are with a read-only copy (for readers on other
+ * threads), without copying them: from then on the list copies an item it
+ * changes, and items it lets go go to retired instead of being freed, since
+ * the copy may still read them. The copy is freed with pure_list_free, on any
+ * thread, when its last reference goes (pure_list_hold takes one more). Only
+ * between events (no change waits to be committed or undone). */
+PureList *pure_list_share(PureList *, PureRetired *retired);
+void pure_list_hold(PureList *copy);
+/* Whether copy shares list's items as they are now. */
+bool pure_list_current(const PureList *copy, const PureList *list);
+/* Free items retired while no copy that shared them remains. */
+void pure_retired_free(PureRetired *);
+/* 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);
+/* In a list ordered by field (a keyed list): the first position whose item's
+ * field does not order below key (numbers by value, texts by bytes), and
+ * whether that item's field is key. */
+size_t pure_list_seek(const PureList *, unsigned field, PureCell key, bool *found);
+/* The list's items ordered by field, as a keyed list keeps them: *out is NULL
+ * when they already are, or a new list in that order with moved[position
+ * before] = position after for every item. CONFLICT when two items have the
+ * same key (nothing is made). */
+PureResult pure_list_sorted(const PureList *, unsigned field, PureList **out, size_t *moved);
+/* A list may keep a stored copy in a draft: header bytes, then each item's
+ * fields in order (a number as its binary64 bits, little-endian; a text as a
+ * u32le length and its bytes). Every change splices the draft as well, and a
+ * change the draft refuses (such as its size limit) does not happen. Undoing
+ * an event changes only the list: the caller restores the draft's version. */
+void pure_list_mirror(PureList *, PureDraft *, size_t header);
+/* Store every item after the header (the draft holds just the header). */
+PureResult pure_list_write(PureList *);
+/* Append the items stored in bytes; each field is checked. Before mirroring. */
+PureResult pure_list_read(PureList *, const unsigned char *bytes, size_t size);
+/* Undo the changes logged after mark (a count taken earlier), newest first. */
+void pure_undo_rollback(PureUndo *, size_t mark);
+void pure_undo_commit(PureUndo *);
+void pure_undo_free(PureUndo *);
+#endif
diff --git a/core/module-save.c b/core/module-save.c
new file mode 100644
index 0000000..4149316
--- /dev/null
+++ b/core/module-save.c
@@ -0,0 +1,488 @@
+#include "module-save.h"
+#include "module-text.h"
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Stored forms. values: "PVS1", then per state {u8 type, u8 name length,
+ * name, binary64 bits (little-endian)}. A list: "PLS1", u8 field count, per
+ * field {u8 0 number | 1 text, u8 name length, name}, then its items
+ * (module-list.h). A text state: its UTF-8 bytes. */
+static const unsigned char values_magic[4] = {'P', 'V', 'S', '1'}, list_magic[4] = {'P', 'L', 'S', '1'};
+/* links: "PLK1"; u8 outputs, each {u8 name length, name, u8 field count, the
+ * fields' types, u64le last number} (the module's outputs, then those it
+ * dropped, without fields); u16le sources, each {u8 length, the name, u64le
+ * last number heard}; then the sent messages to the end, each {u8 output (in
+ * the table), u64le number, u32le size, payload}. */
+static const unsigned char links_magic[4] = {'P', 'L', 'K', '1'};
+static const PureVersion unsaved = {UINT64_MAX, UINT64_MAX};
+
+bool pure_save_enabled(const PureApp *app) { return app->data.state && app->data.save; }
+
+static PureResult open_state(PureApp *app, const char *prefix, const char *name, PureDraft **out) {
+ char key[64];
+ snprintf(key, sizeof(key), "%s%s", prefix, name);
+ return app->data.state(app->data.context, key, out);
+}
+
+/* The whole value of a draft, contiguous; free it. */
+typedef struct { unsigned char *bytes; size_t used; } Whole;
+
+static bool gather(void *context, PureSpan span) {
+ Whole *w = context;
+ memcpy(w->bytes + w->used, span.data, span.size);
+ w->used += span.size;
+ return true;
+}
+
+static PureResult read_whole(const PureDraft *draft, unsigned char **out, size_t *size) {
+ *size = pure_draft_size(draft);
+ Whole w = {malloc(*size ? *size : 1), 0};
+ if (!w.bytes) return PURE_ALLOCATION;
+ PureResult result = pure_draft_read(draft, 0, *size, gather, &w);
+ if (result != PURE_OK) { free(w.bytes); return result; }
+ *out = w.bytes;
+ return PURE_OK;
+}
+
+/* One named entry of a stored form: u8 kind, u8 name length, the name (an
+ * identifier), then width more bytes. */
+PURE_COLD static bool named(const unsigned char *bytes, size_t size, size_t *at, size_t width, uint8_t *kind,
+ char name[PURE_MODULE_NAME]) {
+ if (size - *at < 2) return false;
+ size_t length = bytes[*at + 1];
+ if (size - *at - 2 < length + width || !length || length >= PURE_MODULE_NAME) return false;
+ for (size_t i = 0; i < length; ++i) {
+ unsigned char c = bytes[*at + 2 + i];
+ bool letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
+ if (!letter && !(i && c >= '0' && c <= '9')) return false;
+ }
+ *kind = bytes[*at];
+ memcpy(name, bytes + *at + 2, length);
+ name[length] = 0;
+ *at += 2 + length;
+ return true;
+}
+
+PURE_COLD PureResult pure_save_open_text(PureApp *app, const PureModule *m, uint32_t state, PureDraft **out,
+ PureVersion *saved) {
+ PureResult result = open_state(app, "text-", m->state_names[state], out);
+ if (result != PURE_OK) return result;
+ *saved = pure_draft_version(*out);
+ if (!pure_draft_revision(*out)) {
+ uint32_t constant = m->state_ref[state];
+ PureSpan initial = {m->constant_data + m->constant_offset[constant], m->constant_length[constant]};
+ result = pure_draft_splice(*out, 0, pure_draft_size(*out), initial);
+ *saved = unsaved;
+ } else {
+ unsigned char *bytes;
+ size_t size;
+ result = read_whole(*out, &bytes, &size);
+ if (result == PURE_OK) {
+ if (!pure_utf8_valid((PureSpan){bytes, size})) result = PURE_STORE_ERROR;
+ free(bytes);
+ }
+ }
+ if (result != PURE_OK) { pure_draft_close(*out); *out = NULL; }
+ return result;
+}
+
+static size_t list_header(const PureModule *m, uint32_t record, unsigned char *out) {
+ const PureRecord *r = &m->record[record];
+ size_t at = 0;
+ memcpy(out, list_magic, 4);
+ at = 4;
+ out[at++] = (unsigned char)r->count;
+ for (unsigned f = 0; f < r->count; ++f) {
+ const char *name = m->record_field_names[r->first + f];
+ out[at++] = m->record_field_text[r->first + f];
+ out[at++] = (unsigned char)strlen(name);
+ memcpy(out + at, name, strlen(name));
+ at += strlen(name);
+ }
+ return at;
+}
+
+PURE_COLD PureResult pure_save_rewrite_list(const PureModule *m, uint32_t state, PureList *list, PureDraft *draft) {
+ unsigned char header[5 + PURE_RECORD_FIELDS * (2 + PURE_MODULE_NAME)];
+ size_t length = list_header(m, m->state_ref[state], header);
+ PureVersion before = pure_draft_version(draft);
+ pure_draft_hold(draft, true);
+ PureResult result = pure_draft_splice(draft, 0, pure_draft_size(draft), (PureSpan){header, length});
+ pure_list_mirror(list, draft, length);
+ if (result == PURE_OK) result = pure_list_write(list);
+ if (result != PURE_OK) {
+ pure_draft_restore(draft, before);
+ pure_list_mirror(list, NULL, 0);
+ }
+ pure_draft_hold(draft, false);
+ return result;
+}
+
+PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint32_t state, PureList **out, PureDraft **draft,
+ PureVersion *saved) {
+ uint32_t record = m->state_ref[state];
+ const PureRecord *r = &m->record[record];
+ *out = NULL;
+ PureResult result = open_state(app, "list-", m->state_names[state], draft);
+ if (result != PURE_OK) return result;
+ unsigned char *bytes = NULL;
+ size_t size = 0;
+ if (pure_draft_revision(*draft)) result = read_whole(*draft, &bytes, &size);
+ /* The stored record: its fields' kinds and names. */
+ char names[PURE_RECORD_FIELDS][PURE_MODULE_NAME] = {{0}};
+ uint8_t text[PURE_RECORD_FIELDS] = {0};
+ unsigned count = 0;
+ size_t at = 0;
+ bool stored = result == PURE_OK && bytes;
+ if (stored) {
+ bool ok = size >= 5 && !memcmp(bytes, list_magic, 4) && bytes[4] <= PURE_RECORD_FIELDS;
+ count = ok ? bytes[4] : 0;
+ at = 5;
+ for (unsigned f = 0; ok && f < count; ++f) {
+ ok = named(bytes, size, &at, 0, &text[f], names[f]) && text[f] <= 1;
+ for (unsigned k = 0; k < f; ++k) ok = ok && strcmp(names[k], names[f]);
+ }
+ if (!ok) result = PURE_STORE_ERROR;
+ }
+ PureList *from = NULL;
+ if (result == PURE_OK && stored) {
+ from = pure_list_new(count, text);
+ result = from ? pure_list_read(from, bytes + at, size - at) : PURE_ALLOCATION;
+ }
+ free(bytes);
+ bool same = stored && count == r->count;
+ for (unsigned f = 0; same && f < count; ++f)
+ same = text[f] == m->record_field_text[r->first + f] && !strcmp(names[f], m->record_field_names[r->first + f]);
+ if (result == PURE_OK && same) {
+ *out = from;
+ pure_list_mirror(from, *draft, at);
+ *saved = pure_draft_version(*draft);
+ return PURE_OK;
+ }
+ if (result == PURE_OK) {
+ /* New, or stored with another record: its items move by field name. */
+ const char *field_names[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < count; ++f) field_names[f] = names[f];
+ if (from) result = pure_list_migrate(out, from, field_names, text, count, m, record);
+ else if (!(*out = pure_list_new(r->count, m->record_field_text + r->first))) result = PURE_ALLOCATION;
+ }
+ pure_list_free(from);
+ if (result == PURE_OK) result = pure_save_rewrite_list(m, state, *out, *draft);
+ if (result != PURE_OK) {
+ pure_list_free(*out);
+ pure_draft_close(*draft);
+ *out = NULL;
+ *draft = NULL;
+ return result;
+ }
+ *saved = unsaved;
+ return PURE_OK;
+}
+
+static bool scalar(uint8_t type) {
+ return type == PURE_STATE_NUM || type == PURE_STATE_CURSOR || type == PURE_STATE_INDEX;
+}
+
+/* A stored value still fits its state: a cursor on a character boundary of
+ * its text, an index on an item of its list (or 0), a finite number. */
+PURE_COLD static bool fits(const PureModule *m, PureDraft *const *texts, PureList *const *lists, uint32_t state, double value) {
+ if (!isfinite(value)) return false;
+ if (m->state_type[state] == PURE_STATE_NUM) return true;
+ if (value < 0 || value != floor(value)) return false;
+ if (m->state_type[state] == PURE_STATE_INDEX) {
+ size_t count = pure_list_count(lists[m->state_ref[state]]);
+ return value <= (count ? (double)(count - 1) : 0);
+ }
+ const PureDraft *text = texts[m->state_ref[state]];
+ size_t size = pure_draft_size(text), at = (size_t)value;
+ if (value > (double)size) return false;
+ if (at == size) return true;
+ unsigned char byte = 0;
+ Whole w = {&byte, 0};
+ return pure_draft_read(text, at, 1, gather, &w) == PURE_OK && (byte & 0xC0) != 0x80;
+}
+
+static size_t encode_values(const PureModule *m, const double *values, unsigned char *out) {
+ size_t at = 4;
+ memcpy(out, values_magic, 4);
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (!scalar(m->state_type[i])) continue;
+ size_t length = strlen(m->state_names[i]);
+ uint64_t bits;
+ memcpy(&bits, &values[i], sizeof(bits));
+ out[at++] = m->state_type[i];
+ out[at++] = (unsigned char)length;
+ memcpy(out + at, m->state_names[i], length);
+ at += length;
+ for (unsigned b = 0; b < 8; ++b) out[at++] = (unsigned char)(bits >> (8 * b));
+ }
+ return at;
+}
+
+enum { VALUES_BYTES = 4 + PURE_MODULE_STATES * (2 + PURE_MODULE_NAME + 8) };
+
+PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, double *values, const int *kept,
+ PureDraft *const *texts, PureList *const *lists) {
+ bool scalars = false, fresh = false;
+ for (uint32_t i = 0; i < m->states; ++i)
+ if (scalar(m->state_type[i])) {
+ scalars = true;
+ fresh = fresh || kept[i] < 0;
+ }
+ if (!scalars) return PURE_OK;
+ PureResult result = app->stored_values ? PURE_OK : open_state(app, "", "values", &app->stored_values);
+ if (result != PURE_OK || !fresh || !pure_draft_revision(app->stored_values)) return result;
+ unsigned char *bytes;
+ size_t size;
+ if ((result = read_whole(app->stored_values, &bytes, &size)) != PURE_OK) return result;
+ bool ok = size >= 4 && !memcmp(bytes, values_magic, 4);
+ for (size_t at = 4; ok && at < size;) {
+ char name[PURE_MODULE_NAME];
+ uint8_t type;
+ if (!(ok = named(bytes, size, &at, 8, &type, name))) break;
+ uint64_t bits = 0;
+ for (unsigned b = 0; b < 8; ++b) bits |= (uint64_t)bytes[at + b] << (8 * b);
+ at += 8;
+ double value;
+ memcpy(&value, &bits, sizeof(value));
+ int state = pure_module_state(m, name);
+ if (state >= 0 && kept[state] < 0 && m->state_type[state] == type && scalar(type) &&
+ fits(m, texts, lists, (uint32_t)state, value))
+ values[state] = value;
+ }
+ free(bytes);
+ return ok ? PURE_OK : PURE_STORE_ERROR;
+}
+
+static bool same_version(PureVersion a, PureVersion b) { return a.ref == b.ref && a.bytes == b.bytes; }
+
+static unsigned char *put_le(unsigned char *at, uint64_t value, unsigned width) {
+ for (unsigned i = 0; i < width; ++i) *at++ = (unsigned char)(value >> (8 * i));
+ return at;
+}
+
+static uint64_t get_le(const unsigned char *at, unsigned width) {
+ uint64_t value = 0;
+ for (unsigned i = 0; i < width; ++i) value |= (uint64_t)at[i] << (8 * i);
+ return value;
+}
+
+/* The links as stored; NULL when memory runs out. */
+static unsigned char *encode_links(const PureApp *app, size_t *size) {
+ const PureModule *m = app->module;
+ unsigned outputs = 0;
+ uint8_t table[PURE_MODULE_INPUTS];
+ size_t total = 4 + 1 + 2;
+ for (uint32_t i = 0; i < m->inputs; ++i)
+ if (m->input[i].kind == PURE_KIND_OUTPUT) {
+ table[i] = (uint8_t)outputs++;
+ total += 2 + strlen(m->input_names[i]) + m->input[i].field_count + 8;
+ }
+ for (unsigned c = 0; c < app->carried_count; ++c) total += 2 + strlen(app->carried[c].name) + 8;
+ for (unsigned h = 0; h < app->heard_count; ++h) total += 1 + strlen(app->heard[h].source) + 8;
+ for (size_t s = 0; s < app->sent_count; ++s)
+ if (!app->sent[s].trimmed) total += 1 + 8 + 4 + app->sent[s].size;
+ unsigned char *bytes = malloc(total), *at = bytes;
+ if (!bytes) return NULL;
+ memcpy(at, links_magic, 4);
+ at += 4;
+ *at++ = (unsigned char)(outputs + app->carried_count);
+ for (uint32_t i = 0; i < m->inputs; ++i) {
+ if (m->input[i].kind != PURE_KIND_OUTPUT) continue;
+ size_t length = strlen(m->input_names[i]);
+ *at++ = (unsigned char)length;
+ memcpy(at, m->input_names[i], length);
+ at += length;
+ *at++ = (unsigned char)m->input[i].field_count;
+ for (uint32_t f = 0; f < m->input[i].field_count; ++f) *at++ = m->field[m->input[i].field_start + f].type;
+ at = put_le(at, app->last_sent[i], 8);
+ }
+ for (unsigned c = 0; c < app->carried_count; ++c) {
+ size_t length = strlen(app->carried[c].name);
+ *at++ = (unsigned char)length;
+ memcpy(at, app->carried[c].name, length);
+ at += length;
+ *at++ = 0;
+ at = put_le(at, app->carried[c].last, 8);
+ }
+ at = put_le(at, app->heard_count, 2);
+ for (unsigned h = 0; h < app->heard_count; ++h) {
+ size_t length = strlen(app->heard[h].source);
+ *at++ = (unsigned char)length;
+ memcpy(at, app->heard[h].source, length);
+ at = put_le(at + length, app->heard[h].seq, 8);
+ }
+ for (size_t s = 0; s < app->sent_count; ++s) {
+ const struct PureSent *sent = &app->sent[s];
+ if (sent->trimmed) continue;
+ *at++ = table[sent->output];
+ at = put_le(at, sent->seq, 8);
+ at = put_le(at, sent->size, 4);
+ if (sent->size) memcpy(at, sent->payload, sent->size);
+ at += sent->size;
+ }
+ *size = (size_t)(at - bytes);
+ return bytes;
+}
+
+/* The stored links of a starting app: outputs by name (an output whose
+ * fields changed or that the module lacks refuses the start while it has
+ * sends left: CONFLICT; one without sends keeps its number for later),
+ * sources and sent messages. */
+PURE_COLD PureResult pure_save_open_links(PureApp *app, const PureModule *m) {
+ PureResult result = app->stored_links ? PURE_OK : open_state(app, "", "links", &app->stored_links);
+ if (result != PURE_OK || !pure_draft_revision(app->stored_links)) return result;
+ unsigned char *bytes;
+ size_t size;
+ if ((result = read_whole(app->stored_links, &bytes, &size)) != PURE_OK) return result;
+ int output[256];
+ bool fits[256];
+ size_t at = 5;
+ unsigned outputs = size >= 5 && !memcmp(bytes, links_magic, 4) ? bytes[4] : 0;
+ result = size >= 5 && !memcmp(bytes, links_magic, 4) ? PURE_OK : PURE_STORE_ERROR;
+ for (unsigned t = 0; t < outputs && result == PURE_OK; ++t) {
+ char name[PURE_MODULE_NAME];
+ size_t length = at < size ? bytes[at] : 0;
+ if (!length || length >= PURE_MODULE_NAME || size - at < 2 + length) { result = PURE_STORE_ERROR; break; }
+ memcpy(name, bytes + at + 1, length);
+ name[length] = 0;
+ unsigned fields = bytes[at + 1 + length];
+ at += 2 + length;
+ if (size - at < fields + 8) { result = PURE_STORE_ERROR; break; }
+ int i = pure_module_input(m, name);
+ output[t] = i >= 0 && m->input[i].kind == PURE_KIND_OUTPUT ? i : -1;
+ fits[t] = output[t] >= 0 && m->input[i].field_count == fields;
+ for (unsigned f = 0; fits[t] && f < fields; ++f) fits[t] = m->field[m->input[i].field_start + f].type == bytes[at + f];
+ uint64_t last = get_le(bytes + at + fields, 8);
+ at += fields + 8;
+ if (output[t] >= 0) app->last_sent[output[t]] = last;
+ else if (last) {
+ struct PureCarried *grown = realloc(app->carried, (app->carried_count + 1) * sizeof(*grown));
+ if (!grown) { result = PURE_ALLOCATION; break; }
+ app->carried = grown;
+ memcpy(grown[app->carried_count].name, name, length + 1);
+ grown[app->carried_count++].last = last;
+ }
+ }
+ unsigned sources = result == PURE_OK && size - at >= 2 ? (unsigned)get_le(bytes + at, 2) : 0;
+ if (result == PURE_OK && size - at < 2) result = PURE_STORE_ERROR;
+ at += 2;
+ if (result == PURE_OK && sources && !(app->heard = calloc(sources, sizeof(*app->heard)))) result = PURE_ALLOCATION;
+ app->heard_capacity = sources;
+ for (unsigned h = 0; h < sources && result == PURE_OK; ++h) {
+ size_t length = at < size ? bytes[at] : 0;
+ if (!length || length > PURE_SOURCE || size - at < 1 + length + 8) { result = PURE_STORE_ERROR; break; }
+ struct PureHeard *heard = &app->heard[app->heard_count++];
+ memcpy(heard->source, bytes + at + 1, length);
+ heard->seq = heard->saved = get_le(bytes + at + 1 + length, 8);
+ at += 1 + length + 8;
+ }
+ while (result == PURE_OK && at < size) {
+ if (size - at < 13 || bytes[at] >= outputs) { result = PURE_STORE_ERROR; break; }
+ unsigned t = bytes[at];
+ uint64_t seq = get_le(bytes + at + 1, 8);
+ size_t length = (size_t)get_le(bytes + at + 9, 4);
+ at += 13;
+ if (size - at < length) { result = PURE_STORE_ERROR; break; }
+ if (!fits[t]) { result = PURE_CONFLICT; break; }
+ if (app->sent_count == app->sent_capacity) {
+ size_t capacity = app->sent_capacity ? 2 * app->sent_capacity : 16;
+ struct PureSent *grown = realloc(app->sent, capacity * sizeof(*grown));
+ if (!grown) { result = PURE_ALLOCATION; break; }
+ app->sent = grown;
+ app->sent_capacity = capacity;
+ }
+ unsigned char *payload = malloc(length ? length : 1);
+ if (!payload) { result = PURE_ALLOCATION; break; }
+ if (length) memcpy(payload, bytes + at, length);
+ app->sent[app->sent_count++] = (struct PureSent){(uint32_t)output[t], seq, payload, length, false};
+ at += length;
+ }
+ free(bytes);
+ app->sent_durable = app->sent_count;
+ memcpy(app->saved_sent, app->last_sent, sizeof(app->saved_sent));
+ return result;
+}
+
+PureResult pure_save_commit(PureApp *app, const double *values, void *const *documents, size_t document_count,
+ int64_t *revisions) {
+ if (!pure_save_enabled(app)) {
+ /* Without a host that saves together, a save has one document at most. */
+ PureResult result = PURE_OK;
+ for (size_t i = 0; i < document_count && result == PURE_OK; ++i)
+ result = app->data.commit(app->data.context, documents[i], &revisions[i]);
+ return result;
+ }
+ const PureModule *m = app->module;
+ PureDraft *drafts[2 * PURE_MODULE_STATES + 2];
+ uint32_t states[2 * PURE_MODULE_STATES + 2];
+ size_t count = 0;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ PureDraft *draft = m->state_type[i] == PURE_STATE_TEXT ? app->texts[i] :
+ m->state_type[i] == PURE_STATE_LIST ? app->list_drafts[i] : NULL;
+ if (draft && !same_version(pure_draft_version(draft), app->saved[i])) {
+ states[count] = i;
+ drafts[count++] = draft;
+ }
+ }
+ /* Numbers, cursors and indices: stored again only when their bytes change. */
+ PureVersion values_before = {0, 0};
+ bool values_changed = false;
+ if (app->stored_values) {
+ unsigned char encoded[VALUES_BYTES], *stored;
+ size_t length = encode_values(m, values, encoded), size;
+ PureResult result = read_whole(app->stored_values, &stored, &size);
+ if (result != PURE_OK) return result;
+ values_changed = size != length || memcmp(stored, encoded, length);
+ free(stored);
+ if (values_changed) {
+ values_before = pure_draft_version(app->stored_values);
+ pure_draft_hold(app->stored_values, true);
+ result = pure_draft_splice(app->stored_values, 0, size, (PureSpan){encoded, length});
+ if (result != PURE_OK) { pure_draft_hold(app->stored_values, false); return result; }
+ states[count] = UINT32_MAX;
+ drafts[count++] = app->stored_values;
+ }
+ }
+ /* The links, written whole when they changed. */
+ PureVersion links_before = {0, 0};
+ bool links_written = false;
+ if (app->links_changed) {
+ PureResult result = app->stored_links ? PURE_OK : open_state(app, "", "links", &app->stored_links);
+ size_t length;
+ unsigned char *encoded = result == PURE_OK ? encode_links(app, &length) : NULL;
+ if (result == PURE_OK && !encoded) result = PURE_ALLOCATION;
+ if (result == PURE_OK) {
+ links_before = pure_draft_version(app->stored_links);
+ pure_draft_hold(app->stored_links, true);
+ result = pure_draft_splice(app->stored_links, 0, pure_draft_size(app->stored_links), (PureSpan){encoded, length});
+ if (result != PURE_OK) pure_draft_hold(app->stored_links, false);
+ }
+ free(encoded);
+ if (result != PURE_OK) {
+ if (values_changed) {
+ pure_draft_restore(app->stored_values, values_before);
+ pure_draft_hold(app->stored_values, false);
+ }
+ return result;
+ }
+ links_written = true;
+ states[count] = UINT32_MAX;
+ drafts[count++] = app->stored_links;
+ }
+ if (!count && !document_count) return PURE_OK;
+ PureResult result = app->data.save(app->data.context, documents, document_count, drafts, count, revisions);
+ for (size_t k = 0; result == PURE_OK && k < count; ++k)
+ if (states[k] != UINT32_MAX) app->saved[states[k]] = pure_draft_version(drafts[k]);
+ if (values_changed) {
+ if (result != PURE_OK) pure_draft_restore(app->stored_values, values_before);
+ pure_draft_hold(app->stored_values, false);
+ }
+ if (links_written) {
+ if (result != PURE_OK) pure_draft_restore(app->stored_links, links_before);
+ pure_draft_hold(app->stored_links, false);
+ }
+ return result;
+}
diff --git a/core/module-save.h b/core/module-save.h
new file mode 100644
index 0000000..bc7adcc
--- /dev/null
+++ b/core/module-save.h
@@ -0,0 +1,36 @@
+#ifndef PURE_MODULE_SAVE_H
+#define PURE_MODULE_SAVE_H
+#include "module.h"
+
+/* App states a data host keeps (docs/design/008-module-format.md), internal
+ * to the module executor. Each text state is a stored draft, each list state
+ * a stored draft its list mirrors, and numbers, cursors and indices are one
+ * stored value by name. A save publishes what changed, all or nothing. */
+
+bool pure_save_enabled(const PureApp *);
+/* A text state of m: its stored value, or its initial constant when none. */
+PureResult pure_save_open_text(PureApp *, const PureModule *m, uint32_t state, PureDraft **out, PureVersion *saved);
+/* A list state of m: its stored items, fields moved by name when the stored
+ * record differs from m's. */
+PureResult pure_save_open_list(PureApp *, const PureModule *m, uint32_t state, PureList **out, PureDraft **draft,
+ PureVersion *saved);
+/* Open the stored numbers, cursors and indices, and put each stored one of the
+ * same name and kind that still fits into values, for the states of m that
+ * kept marks as new (-1); texts and lists are m's. */
+PureResult pure_save_open_values(PureApp *, const PureModule *m, double *values, const int *kept,
+ PureDraft *const *texts, PureList *const *lists);
+/* Store a list whose record changed in its draft, header and items anew. */
+PureResult pure_save_rewrite_list(const PureModule *m, uint32_t state, PureList *list, PureDraft *draft);
+/* The stored links of a starting app (module.h, PureApp); CONFLICT when an
+ * output with sends left is missing or has other fields. */
+PureResult pure_save_open_links(PureApp *, const PureModule *m);
+/* Publish the documents and every state changed since the last save
+ * (values are the states' numbers); revisions are the documents'. */
+PureResult pure_save_commit(PureApp *, const double *values, void *const *documents, size_t document_count,
+ int64_t *revisions);
+
+/* A list with record's fields, taking from `from` each field of the same name
+ * and kind and giving the others their defaults (module.c). */
+PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names,
+ const uint8_t *text, unsigned count, const PureModule *m, uint32_t record);
+#endif
diff --git a/core/module-store.c b/core/module-store.c
new file mode 100644
index 0000000..7e759fd
--- /dev/null
+++ b/core/module-store.c
@@ -0,0 +1,88 @@
+#include "module-store.h"
+
+static PureResult open_document(void *context, uint32_t collection, const char *key,
+ void **document, int64_t *revision) {
+ (void)collection; /* v2 modules have one collection: the store's key space */
+ PureDraft *draft;
+ PureResult result = pure_draft_open(context, key, &draft);
+ if (result != PURE_OK) return result;
+ pure_draft_hold(draft, true); /* values read in the event stay readable */
+ *document = draft;
+ *revision = pure_draft_revision(draft);
+ return PURE_OK;
+}
+
+static PureVersion version(void *context, void *document) {
+ (void)context;
+ return pure_draft_version(document);
+}
+
+static PureResult read(void *context, void *document, PureVersion at, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *visit_context) {
+ (void)context;
+ return pure_draft_read_version(document, at, offset, length, visit, visit_context);
+}
+
+static PureResult splice(void *context, void *document, size_t offset, size_t remove, PureSpan insert) {
+ (void)context;
+ return pure_draft_splice(document, offset, remove, insert);
+}
+
+static PureResult commit(void *context, void *document, int64_t *revision) {
+ (void)context;
+ return pure_draft_commit(document, revision);
+}
+
+static void close_document(void *context, void *document) {
+ (void)context;
+ pure_draft_close(document);
+}
+
+static PureResult state(void *context, const char *name, PureDraft **out) {
+ return pure_state_open(context, name, out);
+}
+
+enum { SAVED = PURE_APP_PENDING + 2 * PURE_MODULE_STATES + 1 };
+
+/* The documents and the app's changed states in one tree commit. */
+static PureResult save(void *context, void *const *documents, size_t document_count, PureDraft *const *states,
+ size_t count, int64_t *revisions) {
+ (void)context;
+ PureDraft *drafts[SAVED] = {0};
+ int64_t all[SAVED] = {0};
+ if (document_count > PURE_APP_PENDING || count > SAVED - document_count) return PURE_LIMIT;
+ for (size_t i = 0; i < document_count; ++i) drafts[i] = documents[i];
+ for (size_t i = 0; i < count; ++i) drafts[document_count + i] = states[i];
+ PureResult result = pure_draft_commit_all(drafts, document_count + count, all);
+ for (size_t i = 0; i < document_count; ++i) revisions[i] = all[i];
+ return result;
+}
+
+/* Through a store handle: documents and states share its mapping. */
+static PureResult open_handle_document(void *context, uint32_t collection, const char *key, void **document,
+ int64_t *revision) {
+ (void)collection;
+ PureDraft *draft;
+ PureResult result = pure_store_draft(context, key, &draft);
+ if (result != PURE_OK) return result;
+ pure_draft_hold(draft, true);
+ *document = draft;
+ *revision = pure_draft_revision(draft);
+ return PURE_OK;
+}
+
+static PureResult handle_state(void *context, const char *name, PureDraft **out) {
+ return pure_store_state(context, name, out);
+}
+
+PureData pure_store_data(PureStore *store) {
+ return (PureData){.context = store, .open = open_handle_document, .version = version, .read = read,
+ .splice = splice, .commit = commit, .close = close_document, .state = handle_state,
+ .save = save};
+}
+
+PureData pure_tree_data(const char *path) {
+ return (PureData){.context = (void *)path, .open = open_document, .version = version,
+ .read = read, .splice = splice, .commit = commit, .close = close_document,
+ .state = state, .save = save};
+}
diff --git a/core/module-store.h b/core/module-store.h
new file mode 100644
index 0000000..d14ab3d
--- /dev/null
+++ b/core/module-store.h
@@ -0,0 +1,11 @@
+#ifndef PURE_MODULE_STORE_H
+#define PURE_MODULE_STORE_H
+#include "module.h"
+/* Keyed module collections and the app's states on the selected tree store:
+ * one event opens the message key as a held draft and commits it together
+ * with the states the event changed. The path outlives the app. */
+PureData pure_tree_data(const char *path);
+/* The same through a store handle (state.h): opening a document or state
+ * checks only whether the store changed. The handle outlives the app. */
+PureData pure_store_data(PureStore *);
+#endif
diff --git a/core/module-text.c b/core/module-text.c
new file mode 100644
index 0000000..68c03b0
--- /dev/null
+++ b/core/module-text.c
@@ -0,0 +1,189 @@
+#include "module-text.h"
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum { CHUNK = 4096 };
+
+/* state: pending continuation bytes (bits 0-1) and the allowed range of the
+ * next one (bits 2-4): 0 any, 1 A0-BF after E0, 2 80-9F after ED, 3 90-BF
+ * after F0, 4 80-8F after F4. */
+bool pure_utf8_step(unsigned *state, PureSpan span) {
+ unsigned remaining = *state & 3, range = *state >> 2;
+ for (size_t i = 0; i < span.size; ++i) {
+ /* Eight ASCII bytes at a time between characters. */
+ uint64_t word;
+ while (!remaining && span.size - i >= 8 && (memcpy(&word, span.data + i, 8), !(word & UINT64_C(0x8080808080808080))))
+ i += 8;
+ if (i == span.size) break;
+ unsigned char b = span.data[i];
+ if (remaining) {
+ unsigned low = range == 1 ? 0xA0 : range == 3 ? 0x90 : 0x80;
+ unsigned high = range == 2 ? 0x9F : range == 4 ? 0x8F : 0xBF;
+ if (b < low || b > high) return false;
+ --remaining;
+ range = 0;
+ } else if (b < 0x80) {
+ continue;
+ } else if (b >= 0xC2 && b <= 0xDF) {
+ remaining = 1;
+ } else if (b >= 0xE0 && b <= 0xEF) {
+ remaining = 2;
+ range = b == 0xE0 ? 1 : b == 0xED ? 2 : 0;
+ } else if (b >= 0xF0 && b <= 0xF4) {
+ remaining = 3;
+ range = b == 0xF0 ? 3 : b == 0xF4 ? 4 : 0;
+ } else return false;
+ }
+ *state = remaining | range << 2;
+ return true;
+}
+
+bool pure_utf8_complete(unsigned state) { return (state & 3) == 0; }
+
+bool pure_utf8_valid(PureSpan span) {
+ unsigned state = 0;
+ return pure_utf8_step(&state, span) && pure_utf8_complete(state);
+}
+
+size_t pure_utf8_scalars(PureSpan span) {
+ size_t count = 0;
+ for (size_t i = 0; i < span.size; ++i) count += (span.data[i] & 0xC0) != 0x80;
+ return count;
+}
+
+size_t pure_utf8_newlines(PureSpan span) {
+ size_t count = 0;
+ for (size_t i = 0; i < span.size; ++i) count += span.data[i] == '\n';
+ return count;
+}
+
+size_t pure_format_number(double value, char out[32]) {
+ if (value == 0) { memcpy(out, "0", 2); return 1; }
+ if (value == floor(value) && fabs(value) < 1e15) {
+ /* An integer's digits, without printf (the common case, many times cheaper). */
+ char digits[16];
+ uint64_t n = (uint64_t)fabs(value);
+ size_t count = 0, length = 0;
+ do { digits[count++] = (char)('0' + n % 10); n /= 10; } while (n);
+ if (value < 0) out[length++] = '-';
+ while (count) out[length++] = digits[--count];
+ out[length] = 0;
+ return length;
+ }
+ for (int precision = 1; precision <= 17; ++precision) {
+ snprintf(out, 32, "%.*g", precision, value);
+ if (strtod(out, NULL) == value) break;
+ }
+ char *exponent = strchr(out, 'e');
+ if (exponent) {
+ char *digits = exponent + 2; /* after the sign */
+ while (digits[0] == '0' && digits[1]) memmove(digits, digits + 1, strlen(digits));
+ }
+ return strlen(out);
+}
+
+typedef struct { unsigned char *out; } Copy;
+
+static bool copy(void *context, PureSpan span) {
+ Copy *c = context;
+ memcpy(c->out, span.data, span.size);
+ c->out += span.size;
+ return true;
+}
+
+static void read_bytes(const PureDraft *draft, PureVersion version, size_t offset, size_t length,
+ unsigned char *out) {
+ Copy c = {out};
+ if (length) pure_draft_read_version(draft, version, offset, length, copy, &c);
+}
+
+static size_t forward(const PureDraft *draft, PureVersion version, size_t position) {
+ if (position >= version.bytes) return version.bytes;
+ unsigned char lead;
+ read_bytes(draft, version, position, 1, &lead);
+ size_t length = lead < 0x80 ? 1 : lead < 0xE0 ? 2 : lead < 0xF0 ? 3 : 4;
+ return position + length <= version.bytes ? position + length : version.bytes;
+}
+
+static size_t backward(const PureDraft *draft, PureVersion version, size_t position) {
+ if (!position) return 0;
+ size_t take = position < 4 ? position : 4;
+ unsigned char bytes[4];
+ read_bytes(draft, version, position - take, take, bytes);
+ size_t at = take - 1;
+ while (at && (bytes[at] & 0xC0) == 0x80) --at;
+ return position - take + at;
+}
+
+/* Saturating: stops at the start or end, so work is bounded by the text size. */
+size_t pure_text_step(const PureDraft *draft, PureVersion version, size_t position, double count) {
+ for (; count > 0 && position < version.bytes; --count) position = forward(draft, version, position);
+ for (; count < 0 && position > 0; ++count) position = backward(draft, version, position);
+ return position;
+}
+
+size_t pure_text_line_start(const PureDraft *draft, PureVersion version, size_t position) {
+ unsigned char chunk[CHUNK];
+ while (position) {
+ size_t take = position < CHUNK ? position : CHUNK;
+ read_bytes(draft, version, position - take, take, chunk);
+ for (size_t i = take; i-- > 0;)
+ if (chunk[i] == '\n') return position - take + i + 1;
+ position -= take;
+ }
+ return 0;
+}
+
+size_t pure_text_line_end(const PureDraft *draft, PureVersion version, size_t position) {
+ unsigned char chunk[CHUNK];
+ while (position < version.bytes) {
+ size_t take = version.bytes - position < CHUNK ? version.bytes - position : CHUNK;
+ read_bytes(draft, version, position, take, chunk);
+ for (size_t i = 0; i < take; ++i)
+ if (chunk[i] == '\n') return position + i;
+ position += take;
+ }
+ return version.bytes;
+}
+
+static size_t advance_within(const PureDraft *draft, PureVersion version, size_t position,
+ size_t columns, size_t limit) {
+ for (; columns && position < limit; --columns) position = forward(draft, version, position);
+ return position < limit ? position : limit;
+}
+
+/* Moves keep the column; with no further line the cursor goes to the end or
+ * the start of the text. */
+size_t pure_text_line_step(const PureDraft *draft, PureVersion version, size_t position, double count) {
+ size_t start = pure_text_line_start(draft, version, position), column = 0;
+ for (size_t at = start; at < position; at = forward(draft, version, at)) ++column;
+ for (; count > 0; --count) {
+ size_t end = pure_text_line_end(draft, version, position);
+ if (end == version.bytes) return version.bytes;
+ size_t next = end + 1;
+ position = advance_within(draft, version, next, column, pure_text_line_end(draft, version, next));
+ }
+ for (; count < 0; ++count) {
+ size_t here = pure_text_line_start(draft, version, position);
+ if (!here) return 0;
+ size_t previous = pure_text_line_start(draft, version, here - 1);
+ position = advance_within(draft, version, previous, column, here - 1);
+ }
+ return position;
+}
+
+typedef struct { size_t count; unsigned unit; } Count;
+
+static bool count_span(void *context, PureSpan span) {
+ Count *c = context;
+ c->count += c->unit ? pure_utf8_newlines(span) : pure_utf8_scalars(span);
+ return true;
+}
+
+size_t pure_text_count(const PureDraft *draft, PureVersion version, size_t position, unsigned unit) {
+ Count c = {0, unit};
+ if (position) pure_draft_read_version(draft, version, 0, position, count_span, &c);
+ return c.count + (unit ? 1 : 0);
+}
diff --git a/core/module-text.h b/core/module-text.h
new file mode 100644
index 0000000..4843928
--- /dev/null
+++ b/core/module-text.h
@@ -0,0 +1,34 @@
+#ifndef PURE_MODULE_TEXT_H
+#define PURE_MODULE_TEXT_H
+#include "flow.h"
+#include "state.h"
+#include <stdbool.h>
+#include <stddef.h>
+
+/* Text helpers of the module executor. Text is valid UTF-8; a character is a
+ * Unicode scalar value, so every position these return is a scalar boundary. */
+
+/* Strict UTF-8: no overlongs, surrogates or values past U+10FFFF. state
+ * carries a sequence split across spans; start it at 0 and finish with
+ * pure_utf8_complete. */
+bool pure_utf8_step(unsigned *state, PureSpan span);
+bool pure_utf8_complete(unsigned state);
+bool pure_utf8_valid(PureSpan span);
+
+/* Scalars (bytes that are not 10xxxxxx) and newlines in a span. */
+size_t pure_utf8_scalars(PureSpan span);
+size_t pure_utf8_newlines(PureSpan span);
+
+/* Shortest decimal that reads back to the same finite double: integers
+ * below 1e15 without a point or exponent, otherwise %g form with a trimmed
+ * exponent. -0 prints as 0. Returns the length written (at most 32). */
+size_t pure_format_number(double value, char out[32]);
+
+/* Positions in one version of a text draft (size is version.bytes). */
+size_t pure_text_step(const PureDraft *, PureVersion, size_t position, double count);
+size_t pure_text_line_step(const PureDraft *, PureVersion, size_t position, double count);
+size_t pure_text_line_start(const PureDraft *, PureVersion, size_t position);
+size_t pure_text_line_end(const PureDraft *, PureVersion, size_t position);
+/* Scalars (unit 0) or line number counting from 1 (unit 1) before position. */
+size_t pure_text_count(const PureDraft *, PureVersion, size_t position, unsigned unit);
+#endif
diff --git a/core/module.c b/core/module.c
new file mode 100644
index 0000000..697e918
--- /dev/null
+++ b/core/module.c
@@ -0,0 +1,2122 @@
+#include "module.h"
+#include "module-save.h"
+#include "module-text.h"
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 8,
+ 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, T_KEY for the type of the named keyed list's key. */
+enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8 };
+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, IN_ROW = 32 };
+enum { READ = IN_VIEW | IN_HANDLER };
+/* What an argument names: nothing (it is 0), a state of one type (A_STATE +
+ * the type), a list and one of its record's fields, a message field, the
+ * collection, a constant, a unit, the column's row, a branch target, a stack
+ * depth, a fold, a relation or a keyed list. */
+enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_INDEX + 1, A_FIELD, A_COLLECTION, A_CONSTANT, A_UNIT,
+ A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT };
+/* A fold's block is verified as a column that leaves one number (count) or
+ * two (a value and whether to include it), a table's filter as a column that
+ * leaves one: these stand for a view type. */
+enum { FOLD_ONE = 100, FOLD_TWO, FILTER };
+/* The text or list an opcode works on: its cursor and index operands belong
+ * to it, and so does a cursor or index it pushes. O_ANY: no rule. O_OPERAND:
+ * it pushes a position in its operand's text or list. O_ARG: the argument.
+ * O_REF: the text or list of the argument's cursor or index state. O_LIST:
+ * the list of an item argument. */
+enum { O_ANY, O_OPERAND, O_ARG, O_REF, O_LIST };
+static const unsigned char magic[8] = {'P','U','R','E','M','O','D','5'};
+
+/* Operand types popped (bottom to top), the type pushed, where allowed, what
+ * the argument names and the text or list worked on. */
+#define NUMS(pops) {pops, {T_NUM, T_NUM, T_NUM}, T_NUM, READ, A_NONE, O_ANY}
+#define STATE(type) A_STATE + PURE_STATE_##type
+static const struct { unsigned char pops, in[3], out, where, arg, of; } signature[PURE_OP_COUNT] = {
+ [PURE_OP_CONST] = NUMS(0), [PURE_OP_ADD] = NUMS(2), [PURE_OP_SUB] = NUMS(2), [PURE_OP_MUL] = NUMS(2),
+ [PURE_OP_DIV] = NUMS(2), [PURE_OP_NEG] = NUMS(1), [PURE_OP_LT] = NUMS(2), [PURE_OP_LE] = NUMS(2),
+ [PURE_OP_GT] = NUMS(2), [PURE_OP_GE] = NUMS(2), [PURE_OP_EQ] = NUMS(2), [PURE_OP_NE] = NUMS(2),
+ [PURE_OP_AND] = NUMS(2), [PURE_OP_OR] = NUMS(2), [PURE_OP_NOT] = NUMS(1), [PURE_OP_MIN] = NUMS(2),
+ [PURE_OP_MAX] = NUMS(2), [PURE_OP_CLAMP] = NUMS(3),
+ [PURE_OP_LOAD] = {0, {0}, T_NUM, READ, STATE(NUM), O_ANY},
+ [PURE_OP_DT] = {0, {0}, T_NUM, IN_HANDLER, A_NONE, O_ANY},
+ [PURE_OP_STORE] = {1, {T_NUM}, 0, IN_HANDLER, STATE(NUM), O_ANY},
+ [PURE_OP_JZ] = {1, {T_NUM}, 0, READ, A_TARGET, O_ANY},
+ [PURE_OP_JMP] = {0, {0}, 0, READ, A_TARGET, O_ANY},
+ [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE, A_FIELD, O_ANY},
+ [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED, A_COLLECTION, O_ANY},
+ [PURE_OP_DOC_SIZE] = {0, {0}, T_NUM, IN_KEYED, A_COLLECTION, O_ANY},
+ [PURE_OP_LEN] = {1, {T_BYTES}, T_NUM, IN_HANDLER, A_NONE, O_ANY},
+ [PURE_OP_REPLACE] = {3, {T_NUM, T_NUM, T_BYTES}, 0, IN_KEYED, A_COLLECTION, O_ANY},
+ [PURE_OP_REPLY] = {1, {T_BYTES}, 0, IN_MESSAGE, A_NONE, O_ANY},
+ [PURE_OP_REQUIRE] = {1, {T_NUM}, 0, IN_HANDLER, A_NONE, O_ANY},
+ [PURE_OP_TEXT_CONST] = {0, {0}, T_TEXT, READ, A_CONSTANT, O_ANY},
+ [PURE_OP_TEXT_LOAD] = {0, {0}, T_TEXT, READ, STATE(TEXT), O_ANY},
+ [PURE_OP_TEXT_INPUT] = {0, {0}, T_TEXT, IN_TEXT, A_NONE, O_ANY},
+ [PURE_OP_NUM_TEXT] = {1, {T_NUM}, T_TEXT, READ, A_NONE, O_ANY},
+ [PURE_OP_CONCAT] = {2, {T_TEXT, T_TEXT}, T_TEXT, READ, A_NONE, O_ANY},
+ [PURE_OP_CHARS] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY},
+ [PURE_OP_LINES] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY},
+ [PURE_OP_TEXT_BYTES] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY},
+ [PURE_OP_CUR_LOAD] = {0, {0}, T_CURSOR, READ, STATE(CURSOR), O_REF},
+ [PURE_OP_CUR_START] = {0, {0}, T_CURSOR, READ, STATE(TEXT), O_ARG},
+ [PURE_OP_CUR_END] = {0, {0}, T_CURSOR, READ, STATE(TEXT), O_ARG},
+ [PURE_OP_CUR_MOVE] = {2, {T_CURSOR, T_NUM}, T_CURSOR, READ, A_UNIT, O_OPERAND},
+ [PURE_OP_CUR_STORE] = {1, {T_CURSOR}, 0, IN_HANDLER, STATE(CURSOR), O_REF},
+ [PURE_OP_TEXT_INSERT] = {2, {T_CURSOR, T_TEXT}, 0, IN_HANDLER, STATE(TEXT), O_ARG},
+ [PURE_OP_TEXT_REMOVE] = {2, {T_CURSOR, T_CURSOR}, 0, IN_HANDLER, STATE(TEXT), O_ARG},
+ [PURE_OP_TEXT_LINE] = {1, {T_CURSOR}, T_TEXT, READ, STATE(TEXT), O_ARG},
+ [PURE_OP_TEXT_SET] = {1, {T_TEXT}, 0, IN_HANDLER, STATE(TEXT), O_ANY},
+ [PURE_OP_TEXT_FROM] = {1, {T_BYTES}, T_TEXT, READ, A_NONE, O_ANY},
+ [PURE_OP_BYTES_FROM] = {1, {T_TEXT}, T_BYTES, READ, A_NONE, O_ANY},
+ [PURE_OP_CUR_COUNT] = {1, {T_CURSOR}, T_NUM, READ, A_UNIT, O_ANY},
+ /* Record operations also take as many values as the list's record has fields. */
+ [PURE_OP_LIST_COUNT] = {0, {0}, T_NUM, READ, STATE(LIST), O_ANY},
+ [PURE_OP_LIST_APPEND] = {0, {0}, 0, IN_HANDLER, STATE(LIST), O_ANY},
+ [PURE_OP_LIST_INSERT] = {1, {T_INDEX}, 0, IN_HANDLER, STATE(LIST), O_ARG},
+ [PURE_OP_LIST_REMOVE] = {1, {T_INDEX}, 0, IN_HANDLER, STATE(LIST), O_ARG},
+ [PURE_OP_ITEM_GET] = {1, {T_INDEX}, T_ITEM, READ, A_ITEM, O_LIST},
+ [PURE_OP_ITEM_SET] = {1, {T_INDEX}, 0, IN_HANDLER, A_ITEM, O_LIST},
+ [PURE_OP_INDEX_LOAD] = {0, {0}, T_INDEX, READ, STATE(INDEX), O_REF},
+ [PURE_OP_INDEX_STORE] = {1, {T_INDEX}, 0, IN_HANDLER, STATE(INDEX), O_REF},
+ [PURE_OP_INDEX_MOVE] = {2, {T_INDEX, T_NUM}, T_INDEX, READ, A_NONE, O_OPERAND},
+ [PURE_OP_INDEX_FIRST] = {0, {0}, T_INDEX, READ, STATE(LIST), O_ARG},
+ [PURE_OP_INDEX_LAST] = {0, {0}, T_INDEX, READ, STATE(LIST), O_ARG},
+ [PURE_OP_INDEX_NUM] = {1, {T_INDEX}, T_NUM, READ, A_NONE, O_ANY},
+ [PURE_OP_ROW] = {0, {0}, T_INDEX, IN_ROW, A_ROW, O_ARG},
+ /* Stack copies: their types come from the values they move. */
+ [PURE_OP_PICK] = {0, {0}, 0, READ, A_DEPTH, O_ANY},
+ [PURE_OP_SLIDE] = {0, {0}, 0, READ, A_DEPTH, O_ANY},
+ [PURE_OP_FOLD] = {0, {0}, T_NUM, READ, A_FOLD, O_ANY},
+ [PURE_OP_TEXT_COMPARE] = {2, {T_TEXT, T_TEXT}, T_NUM, READ, A_RELATION, O_ANY},
+ [PURE_OP_LIST_FIND] = {1, {T_KEY}, T_INDEX, READ, A_KEYED, O_ARG},
+ [PURE_OP_LIST_HAS] = {1, {T_KEY}, T_NUM, READ, A_KEYED, O_ANY},
+ [PURE_OP_NOW] = {0, {0}, T_NUM, IN_HANDLER, A_NONE, O_ANY},
+ [PURE_OP_SEND] = {0, {0}, 0, IN_HANDLER, A_OUTPUT, O_ANY},
+ [PURE_OP_FLOOR] = NUMS(1), [PURE_OP_MOD] = NUMS(2),
+};
+#undef NUMS
+#undef STATE
+
+static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; }
+
+static uint32_t read32(const unsigned char *p) {
+ return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
+}
+
+static void write32(unsigned char *p, uint32_t value) {
+ for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(value >> (8 * i));
+}
+
+static double read_double(const unsigned char *p) {
+ uint64_t bits = read32(p) | (uint64_t)read32(p + 4) << 32;
+ double value;
+ memcpy(&value, &bits, sizeof(value));
+ return value;
+}
+
+PURE_COLD static bool key_name(const char *key) {
+ static const char *const named[] = {"Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
+ "Backspace", "Delete", "Home", "End", "Tab", "Escape", "PageUp", "PageDown"};
+ for (size_t i = 0; i < sizeof(named) / sizeof(*named); ++i)
+ if (!strcmp(key, named[i])) return true;
+ if (!strncmp(key, "Key", 3)) return key[3] >= 'A' && key[3] <= 'Z' && !key[4];
+ if (!strncmp(key, "Digit", 5)) return key[5] >= '0' && key[5] <= '9' && !key[6];
+ return false;
+}
+
+/* A document key: 1..64 bytes of [A-Za-z0-9_-], as every Pure store accepts. */
+static bool document_key(const char *key) {
+ size_t length = key ? strlen(key) : 0;
+ if (!length || length > 64) return false;
+ for (size_t i = 0; i < length; ++i) {
+ char c = key[i];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
+ c == '-' || c == '_')) return false;
+ }
+ return true;
+}
+
+/* Strings are [u8 length][bytes]; a name is 1..31 bytes: an ASCII identifier,
+ * a path of identifiers joined by dots (a message field inside a group, as
+ * user.name), or any printable ASCII (a key's name). */
+enum { NAME_IDENTIFIER, NAME_PATH, NAME_PRINTABLE };
+PURE_COLD static bool name_at(const unsigned char *strings, uint32_t size, uint32_t offset,
+ int kind, char out[PURE_MODULE_NAME]) {
+ if (offset >= size) return false;
+ uint32_t length = strings[offset];
+ if (!length || length >= PURE_MODULE_NAME || length > size - offset - 1) return false;
+ for (uint32_t i = 0, start = 0; i < length; ++i) {
+ unsigned char c = strings[offset + 1 + i];
+ bool letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
+ if (kind == NAME_PATH && c == '.' && i > start && i + 1 < length) { start = i + 1; continue; }
+ if (!letter && !(i > start && c >= '0' && c <= '9') && (kind != NAME_PRINTABLE || c < 32 || c > 126))
+ return false;
+ }
+ memcpy(out, strings + offset + 1, length);
+ out[length] = 0;
+ return true;
+}
+
+/* The input whose handler this is, or -1 for frame. */
+static int event_input(uint32_t event) { return event ? (int)((event - 1) / 2) : -1; }
+
+/* The state an argument names must have this type. */
+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;
+}
+
+PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg, const PureInputShape *shape, int row) {
+ switch (kind) {
+ case A_ITEM:
+ return state_of(m, list_of(arg), PURE_STATE_LIST) && item_field(arg) < m->record[m->state_ref[list_of(arg)]].count;
+ case A_FIELD: return arg < shape->field_count;
+ case A_COLLECTION: return arg < m->collections;
+ case A_CONSTANT: return arg < m->constants;
+ case A_UNIT: return arg <= PURE_UNIT_LINE;
+ case A_ROW: return (int)arg == row;
+ case A_FOLD: return (arg & 0xFF) <= PURE_FOLD_FIRST && state_of(m, arg >> 8 & 0xFF, PURE_STATE_LIST) && arg >> 16;
+ case A_RELATION: return arg <= PURE_NOT_EQUAL;
+ case A_KEYED: return state_of(m, arg, PURE_STATE_LIST) && m->state_key[arg];
+ case A_OUTPUT: return arg < m->inputs && m->input[arg].kind == PURE_KIND_OUTPUT;
+ case A_NONE: case A_TARGET: case A_DEPTH: return true;
+ default: return state_of(m, arg, (uint8_t)(kind - A_STATE));
+ }
+}
+
+/* The stack a branch brings to its target, while the target is ahead. */
+typedef struct { uint32_t target; int height; unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK]; } Join;
+
+static bool same_stack(const Join *join, int height, const unsigned char *types, const unsigned char *owners) {
+ return join->height == height && !memcmp(join->types, types, (size_t)height) &&
+ !memcmp(join->owners, owners, (size_t)height);
+}
+
+/* Types and heights are checked per instruction; a cursor's type records
+ * which text it belongs to and an index's type which list (other values
+ * belong to nothing). Branches go forward within the block. Every path into
+ * a join brings the same stack (types and owners), so a value may cross a
+ * join, as in a conditional expression; at most 8 joins with a non-empty
+ * stack are ahead at once. Handlers end empty, views with one value of their
+ * type. A column (row is its list) may read the row it is evaluated for.
+ * A fold's block is verified as a column of its list (reads only, no fold
+ * in it); the block around treats it as one instruction and may not branch
+ * into it. Returns why the block is refused, with at moved to the
+ * instruction when one is at fault, or NULL. */
+PURE_COLD static const char *verify_block(const PureModule *, PureBlock, int view, int row, uint32_t event, uint32_t *at);
+
+/* target marks, per instruction of the block, a branch into it with an empty stack. */
+PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, int view, int row, uint32_t event,
+ uint32_t *at, unsigned char *target) {
+#define REFUSE(why) do { *at = pc; return why; } while (0)
+ if (view >= 0 && !block.length) return "empty view";
+ int input = view >= 0 ? -1 : event_input(event);
+ const PureInputShape *shape = input >= 0 ? &m->input[input] : NULL;
+ unsigned where = view >= 0 ? IN_VIEW | (row >= 0 ? IN_ROW : 0) : IN_HANDLER;
+ if (shape && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0);
+ if (shape && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT;
+ unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK];
+ Join joins[PURE_MODULE_JOINS];
+ unsigned open = 0;
+ int height = 0;
+ bool reachable = true;
+ for (uint32_t i = 0; i <= block.length; ++i) {
+ uint32_t pc = block.start + i;
+ /* Branches here brought an empty stack (target) or the stack of a join. */
+ unsigned join = open;
+ for (unsigned k = 0; k < open; ++k) if (joins[k].target == pc) join = k;
+ if (join < open) {
+ if (target[i] || (reachable && !same_stack(&joins[join], height, types, owners)))
+ REFUSE("join with different stacks");
+ height = joins[join].height;
+ memcpy(types, joins[join].types, (size_t)height);
+ memcpy(owners, joins[join].owners, (size_t)height);
+ joins[join] = joins[--open];
+ reachable = true;
+ } else if (target[i]) {
+ if (reachable && height) REFUSE("join with different stacks");
+ height = 0;
+ reachable = true;
+ }
+ if (i == block.length) break;
+ if (!reachable) REFUSE("unreachable instruction");
+ const PureOp *op = &m->code[pc];
+ if (op->op >= PURE_OP_COUNT) REFUSE("unknown opcode");
+ unsigned allowed = signature[op->op].where, kind = signature[op->op].arg, of = signature[op->op].of;
+ if (!(allowed & where)) {
+ if (allowed & IN_ROW) REFUSE("row outside a column");
+ if (view == FILTER) REFUSE("filter with an effect");
+ if (view >= FOLD_ONE) REFUSE("fold with an effect");
+ if (view >= 0) REFUSE("view with an effect");
+ if (allowed & IN_KEYED) REFUSE("document operation outside a keyed message handler");
+ if (allowed & IN_TEXT) REFUSE("text input outside a text handler");
+ REFUSE("message operation outside a message handler");
+ }
+ uint32_t arg = op->arg;
+ if ((kind == A_NONE && arg) || (op->op == PURE_OP_CONST ? !isfinite(op->value) : op->value != 0))
+ REFUSE("non-canonical operand");
+ if (!names_arg(m, kind, arg, shape, row)) REFUSE("operand names no such state, field, constant or unit");
+ /* A keyed list places its items by key, and an item's key stays. */
+ if (op->op == PURE_OP_LIST_INSERT && m->state_key[arg]) REFUSE("insert by position into a keyed list");
+ if (op->op == PURE_OP_ITEM_SET && m->state_key[list_of(arg)] == item_field(arg) + 1)
+ REFUSE("change of a keyed list's key");
+ /* PICK copies the value arg below the top; SLIDE drops arg values under the top. */
+ if (kind == A_DEPTH) {
+ bool pick = op->op == PURE_OP_PICK;
+ if (pick ? arg >= (uint32_t)height : !arg || arg >= (uint32_t)height) REFUSE("stack underflow");
+ if (pick && height == PURE_MODULE_STACK) REFUSE("stack overflow");
+ int from = height - 1 - (int)arg, to = pick ? height++ : from;
+ types[to] = pick ? types[from] : types[height - 1];
+ owners[to] = pick ? owners[from] : owners[height - 1];
+ if (!pick) height = from + 1;
+ continue;
+ }
+ uint32_t skip = 0;
+ if (kind == A_FOLD) {
+ uint32_t inner = pc;
+ skip = arg >> 16;
+ if (view >= FOLD_ONE) REFUSE(view == FILTER ? "fold inside a filter" : "fold inside a fold");
+ if (skip > block.length - i - 1) REFUSE("fold block outside its block");
+ unsigned how = arg & 0xFF;
+ const char *why = verify_block(m, (PureBlock){pc + 1, skip},
+ how == PURE_FOLD_COUNT || how == PURE_FOLD_FIRST ? FOLD_ONE : FOLD_TWO,
+ (int)(arg >> 8 & 0xFF), 0, &inner);
+ if (why) { *at = inner; return why; }
+ for (uint32_t k = i + 1; k <= i + skip; ++k) {
+ if (target[k]) REFUSE("branch into a fold");
+ for (unsigned j = 0; j < open; ++j) if (joins[j].target == block.start + k) REFUSE("branch into a fold");
+ }
+ }
+ /* Operand types, bottom to top; record operations take their fields. */
+ unsigned char need[PURE_MODULE_FIELDS + 2];
+ unsigned pops = signature[op->op].pops;
+ memcpy(need, signature[op->op].in, sizeof(signature[op->op].in));
+ if (op->op == PURE_OP_SEND) {
+ const PureInputShape *output = &m->input[arg];
+ for (unsigned f = 0; f < output->field_count; ++f) {
+ uint8_t type = m->field[output->field_start + f].type;
+ need[f] = type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : T_NUM;
+ }
+ pops = output->field_count;
+ } else 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));
+ } else if (need[0] == T_KEY) {
+ need[0] = field_type(m, arg, m->state_key[arg] - 1u);
+ }
+ if (height < (int)pops) REFUSE("stack underflow");
+ height -= (int)pops;
+ for (unsigned k = 0; k < pops; ++k)
+ if (types[height + (int)k] != need[k]) REFUSE("operand type");
+ unsigned owner = of == O_ARG ? arg : of == O_REF ? m->state_ref[arg] : of == O_LIST ? list_of(arg) : 0;
+ for (unsigned k = 0; of >= O_ARG && k < pops; ++k) {
+ unsigned char type = types[height + (int)k];
+ if ((type == T_CURSOR || type == T_INDEX) && owners[height + (int)k] != owner)
+ REFUSE(type == T_CURSOR ? "cursor of another text" : "index of another list");
+ }
+ unsigned char out = signature[op->op].out;
+ if (kind == A_FOLD && (arg & 0xFF) == PURE_FOLD_FIRST) { /* an index of its list */
+ out = T_INDEX;
+ of = O_ARG;
+ owner = arg >> 8 & 0xFF;
+ }
+ if (out == T_FIELD) {
+ uint8_t type = m->field[shape->field_start + arg].type;
+ out = type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : T_NUM;
+ }
+ if (out == T_ITEM) out = field_type(m, list_of(arg), item_field(arg));
+ if (out) {
+ if (height == PURE_MODULE_STACK) REFUSE("stack overflow");
+ owners[height] = (unsigned char)(out != T_CURSOR && out != T_INDEX ? 0 : of == O_OPERAND ? owners[height] : owner);
+ types[height++] = out;
+ }
+ if (kind == A_TARGET) {
+ if (arg <= pc || arg > block.start + block.length) REFUSE("branch not forward within its block");
+ join = open;
+ for (unsigned k = 0; k < open; ++k) if (joins[k].target == arg) join = k;
+ if (!height) {
+ if (join < open) REFUSE("join with different stacks");
+ target[arg - block.start] = 1;
+ } else if (join < open) {
+ if (!same_stack(&joins[join], height, types, owners)) REFUSE("join with different stacks");
+ } else {
+ if (target[arg - block.start]) REFUSE("join with different stacks");
+ if (open == PURE_MODULE_JOINS) REFUSE("more than 8 joins ahead");
+ joins[open] = (Join){.target = arg, .height = height};
+ memcpy(joins[open].types, types, (size_t)height);
+ memcpy(joins[open++].owners, owners, (size_t)height);
+ }
+ if (op->op == PURE_OP_JMP) reachable = false;
+ }
+ i += skip;
+ }
+ if (!reachable) return "block end is unreachable";
+ if (view == FILTER) return height != 1 || types[0] != T_NUM ? "filter must leave one number" : NULL;
+ if (view >= FOLD_ONE) {
+ int want = view - FOLD_ONE + 1;
+ return height != want || types[0] != T_NUM || types[want - 1] != T_NUM ? "fold block must leave its numbers" : NULL;
+ }
+ unsigned char result = view == PURE_VIEW_TEXT ? T_TEXT : T_NUM;
+ if (view >= 0 ? height != 1 || types[0] != result : height != 0)
+ return view >= 0 ? "view must leave one value of its type" : "handler must leave no value";
+ return NULL;
+#undef REFUSE
+}
+
+PURE_COLD static const char *verify_block(const PureModule *m, PureBlock block, int view, int row, uint32_t event,
+ uint32_t *at) {
+ if (block.start > m->code_count || block.length > m->code_count - block.start) return "code range outside module";
+ unsigned char *target = calloc((size_t)block.length + 1, 1);
+ if (!target) return "out of memory";
+ const char *why = verify_code(m, block, view, row, event, at, target);
+ free(target);
+ return why;
+}
+
+PURE_COLD PureResult pure_module_read(const char *path, PureModule *out, char *error, size_t error_size) {
+ FILE *file = fopen(path, "rb");
+ unsigned char *bytes = NULL;
+ size_t size = 0, capacity = 0;
+ bool ok = file != NULL;
+ /* The largest module is some 2 MiB: code, strings and constants at their limits. */
+ while (ok && !feof(file) && capacity < (8u << 20)) {
+ capacity = capacity ? capacity * 2 : 1 << 16;
+ unsigned char *grown = realloc(bytes, capacity);
+ if (!grown) { ok = false; break; }
+ bytes = grown;
+ size += fread(bytes + size, 1, capacity - size, file);
+ ok = !ferror(file);
+ }
+ ok = ok && feof(file);
+ if (file) fclose(file);
+ PureResult result = PURE_NOT_FOUND;
+ if (!ok) {
+ if (error_size) snprintf(error, error_size, "cannot read %s", path);
+ pure_module_release(out);
+ } else result = pure_module_load(bytes, size, out, error, error_size);
+ free(bytes);
+ return result;
+}
+
+void pure_module_release(PureModule *m) {
+ free(m->storage);
+ m->storage = NULL;
+ m->code = NULL;
+ m->constant_offset = m->constant_length = NULL;
+ m->constant_data = NULL;
+}
+
+PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
+ char *error, size_t error_size) {
+ const unsigned char *p = bytes;
+ const char *why;
+ uint32_t bad = 0;
+ PureResult result = PURE_INVALID;
+#define REFUSE(message, index) do { why = message; bad = (uint32_t)(index); goto refused; } while (0)
+ if (error_size) error[0] = 0;
+ pure_module_release(out);
+ memset(out, 0, sizeof(*out));
+ if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic))) REFUSE("not a Pure module v5", 0);
+ out->states = (uint16_t)read16(p + 8);
+ out->inputs = (uint16_t)read16(p + 10);
+ out->views = (uint16_t)read16(p + 12);
+ out->handlers = (uint16_t)read16(p + 14);
+ out->code_count = read32(p + 16);
+ uint32_t strings = read32(p + 20);
+ 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);
+ out->record_fields = (uint16_t)read16(p + 36);
+ if (read16(p + 38)) REFUSE("reserved header bytes", 38);
+ 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->records > PURE_MODULE_RECORDS || out->record_fields > PURE_MODULE_RECORD_FIELDS) {
+ result = PURE_LIMIT;
+ REFUSE("module exceeds a limit", 8);
+ }
+ 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) REFUSE("module size", expected);
+ /* The code, the constants' offsets and lengths, then their bytes, in one block. */
+ size_t storage = (size_t)out->code_count * sizeof(PureOp) + (size_t)out->constants * 2 * sizeof(uint32_t) +
+ constant_bytes;
+ if (!(out->storage = malloc(storage ? storage : 1))) {
+ result = PURE_ALLOCATION;
+ REFUSE("out of memory", 0);
+ }
+ out->code = out->storage;
+ out->constant_offset = (uint32_t *)(out->code + out->code_count);
+ out->constant_length = out->constant_offset + out->constants;
+ out->constant_data = (unsigned char *)(out->constant_length + out->constants);
+ const unsigned char *at = p + PURE_MODULE_HEADER;
+ const unsigned char *text = p + size - constant_bytes - strings;
+ if (constant_bytes) memcpy(out->constant_data, p + size - constant_bytes, constant_bytes);
+ for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) {
+ out->state_type[i] = (uint8_t)read32(at + 4);
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->state_names[i])) REFUSE("state name", i);
+ if (read32(at + 4) > PURE_STATE_INDEX) REFUSE("state type", i);
+ if (out->state_type[i] == PURE_STATE_NUM) {
+ out->initial[i] = read_double(at + 8);
+ if (!isfinite(out->initial[i])) REFUSE("non-finite initial value", i);
+ } else {
+ /* Flags: a mark (cursor), or a keyed list's key field + 1. */
+ uint32_t flags = read32(at + 12);
+ bool cursor = out->state_type[i] == PURE_STATE_CURSOR, list = out->state_type[i] == PURE_STATE_LIST;
+ if ((flags && !(cursor && flags == PURE_CURSOR_STAYS) && !(list && flags <= PURE_RECORD_FIELDS)) ||
+ read32(at + 8) > UINT16_MAX) REFUSE("state reference", i);
+ out->state_ref[i] = (uint16_t)read32(at + 8);
+ out->state_flags[i] = cursor ? (uint8_t)flags : 0;
+ out->state_key[i] = list ? (uint8_t)flags : 0;
+ }
+ }
+ for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) {
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->collection_names[i])) REFUSE("collection name", i);
+ if (read32(at + 4) != 1 || read32(at + 8) != 1) REFUSE("collection must hold bytes by key", i);
+ }
+ uint32_t next_field = 0;
+ const unsigned char *fields = at + (size_t)out->inputs * INPUT_BYTES;
+ for (uint32_t i = 0; i < out->inputs; ++i, at += INPUT_BYTES) {
+ PureInputShape *shape = &out->input[i];
+ shape->kind = at[4];
+ shape->keyed = at[5];
+ shape->field_start = (uint16_t)read16(at + 12);
+ shape->field_count = (uint16_t)read16(at + 14);
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->input_names[i])) REFUSE("input name", i);
+ if (read16(at + 6) || at[5] > 1) REFUSE("input flags", i);
+ if (shape->kind == PURE_KIND_KEY) {
+ if (shape->keyed || shape->field_start || shape->field_count ||
+ !name_at(text, strings, read32(at + 8), NAME_PRINTABLE, out->input_keys[i]) ||
+ !key_name(out->input_keys[i])) REFUSE("input key", i);
+ for (uint32_t k = 0; k < i; ++k)
+ if (out->input[k].kind == PURE_KIND_KEY && !strcmp(out->input_keys[k], out->input_keys[i]))
+ REFUSE("duplicate key", i);
+ continue;
+ }
+ if (shape->kind == PURE_KIND_TEXT) {
+ if (shape->keyed || shape->field_start || shape->field_count || read32(at + 8))
+ REFUSE("text input shape", i);
+ continue;
+ }
+ if (shape->kind == PURE_KIND_TIMER) {
+ shape->period = read32(at + 8);
+ if (shape->keyed || shape->field_start || shape->field_count || !shape->period ||
+ shape->period > PURE_TIMER_LONGEST) REFUSE("timer input", i);
+ continue;
+ }
+ if ((shape->kind != PURE_KIND_MESSAGE && shape->kind != PURE_KIND_OUTPUT) || read32(at + 8) ||
+ (shape->kind == PURE_KIND_OUTPUT && shape->keyed) || shape->field_start != next_field ||
+ shape->field_count > out->fields - next_field) REFUSE("message shape", i);
+ /* The prefix holds the fixed fields and the variable fields' lengths
+ * in order, but the last variable field's: it takes the rest. */
+ uint32_t last = UINT32_MAX;
+ for (uint32_t f = 0; f < shape->field_count; ++f) {
+ uint32_t type = read32(fields + (size_t)(next_field + f) * FIELD_BYTES + 4);
+ if (type == PURE_FIELD_BYTES || type == PURE_FIELD_TEXT) last = f;
+ }
+ for (uint32_t f = 0; f < shape->field_count; ++f) {
+ const unsigned char *entry = fields + (size_t)(next_field + f) * FIELD_BYTES;
+ PureField *field = &out->field[next_field + f];
+ field->type = (uint8_t)read32(entry + 4);
+ if (!name_at(text, strings, read32(entry), NAME_PATH, field->name) || field->type < PURE_FIELD_U32 ||
+ read32(entry + 4) > PURE_FIELD_TEXT)
+ REFUSE("message field", next_field + f);
+ /* A name is a field or a group, never both (user and user.name). */
+ for (uint32_t k = 0; k < f; ++k) {
+ const char *a = out->field[next_field + k].name, *b = field->name;
+ size_t la = strlen(a), lb = strlen(b);
+ if (!strcmp(a, b)) REFUSE("duplicate field", next_field + f);
+ if ((la < lb && !memcmp(a, b, la) && b[la] == '.') || (lb < la && !memcmp(a, b, lb) && a[lb] == '.'))
+ REFUSE("field and group", next_field + f);
+ }
+ field->offset = f == last ? PURE_FIELD_REST : shape->prefix;
+ if (f != last) shape->prefix += field->type == PURE_FIELD_NUMBER ? 8 : 4;
+ }
+ next_field += shape->field_count;
+ }
+ if (next_field != out->fields) REFUSE("unused message fields", next_field);
+ at = fields + (size_t)out->fields * FIELD_BYTES;
+ for (uint32_t i = 0; i < out->constants; ++i, at += CONSTANT_BYTES) {
+ out->constant_offset[i] = read32(at);
+ out->constant_length[i] = read32(at + 4);
+ if (out->constant_offset[i] > constant_bytes || out->constant_length[i] > constant_bytes - out->constant_offset[i] ||
+ !pure_utf8_valid((PureSpan){out->constant_data + out->constant_offset[i], out->constant_length[i]}))
+ REFUSE("text constant", i);
+ }
+ /* 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), NAME_IDENTIFIER, out->record_names[i]) || record->first != next_record_field ||
+ !record->count || record->count > PURE_RECORD_FIELDS || record->count > out->record_fields - next_record_field)
+ REFUSE("record", i);
+ 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), NAME_IDENTIFIER, out->record_field_names[f]) || type > 1)
+ REFUSE("record field", f);
+ for (uint32_t k = record->first; k < f; ++k)
+ if (!strcmp(out->record_field_names[k], out->record_field_names[f])) REFUSE("duplicate record field", f);
+ out->record_field_default[f] = type ? read32(entry + 8) : read_double(entry + 8);
+ if (type ? read32(entry + 12) || read32(entry + 8) >= out->constants : !isfinite(out->record_field_default[f]))
+ REFUSE("record field default", f);
+ }
+ next_record_field += record->count;
+ }
+ if (next_record_field != out->record_fields) REFUSE("unused record fields", next_record_field);
+ 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_LIST &&
+ (out->state_ref[i] >= out->records || out->state_key[i] > out->record[out->state_ref[i]].count)) ||
+ (out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST)))
+ REFUSE("state reference", i);
+ /* A table names its list in place of code; its columns follow it. */
+ for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) {
+ if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->view_names[i])) REFUSE("view name", i);
+ out->view_type[i] = at[4];
+ out->view_table[i] = (uint16_t)read16(at + 6);
+ out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)};
+ uint32_t table = out->view_table[i];
+ if (at[4] > PURE_VIEW_TABLE || (at[5] && at[4] != PURE_VIEW_TABLE) ||
+ (table && (table > i || out->view_type[table - 1] != PURE_VIEW_TABLE || at[4] == PURE_VIEW_TABLE)))
+ REFUSE("view type", i);
+ /* A table names its list in start, or, filtered, in the byte after its
+ * type (list + 1) with its filter's block in start and length. */
+ if (at[4] == PURE_VIEW_TABLE) {
+ uint32_t list = at[5] ? at[5] - 1u : out->view_code[i].start;
+ if (!state_of(out, list, PURE_STATE_LIST) || !at[5] != !out->view_code[i].length) REFUSE("table list", i);
+ out->view_list[i] = (uint16_t)list;
+ } else if (table) out->view_list[i] = out->view_list[table - 1];
+ }
+ for (uint32_t i = 0; i < out->handlers; ++i, at += HANDLER_BYTES) {
+ uint32_t event = read32(at);
+ out->handler[i] = (PureHandler){event, {read32(at + 4), read32(at + 8)}};
+ int input = event_input(event);
+ if (event > 2u * out->inputs || (input >= 0 && out->input[input].kind != PURE_KIND_KEY && !(event & 1)) ||
+ (input >= 0 && out->input[input].kind == PURE_KIND_OUTPUT))
+ REFUSE("handler event", i);
+ }
+ for (uint32_t i = 0; i < out->code_count; ++i, at += OP_BYTES) {
+ if (at[1] || at[2] || at[3]) REFUSE("instruction padding", i);
+ out->code[i] = (PureOp){.op = at[0], .arg = read32(at + 4), .value = read_double(at + 8)};
+ }
+ /* 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) if (!out->view_table[i]) names[count++] = out->view_names[i];
+ for (size_t i = 0; i < count; ++i)
+ for (size_t k = 0; k < i; ++k)
+ if (!strcmp(names[i], names[k])) REFUSE("duplicate name", i);
+ /* Column names are unique within their table. */
+ for (uint32_t i = 0; i < out->views; ++i)
+ for (uint32_t k = 0; k < i; ++k)
+ if (out->view_table[i] && out->view_table[k] == out->view_table[i] &&
+ !strcmp(out->view_names[i], out->view_names[k])) REFUSE("duplicate column", i);
+ for (uint32_t i = 0; i < out->views; ++i) {
+ bad = i;
+ bool table = out->view_type[i] == PURE_VIEW_TABLE;
+ if ((!table || out->view_code[i].length) &&
+ (why = verify_block(out, out->view_code[i], table ? FILTER : out->view_type[i],
+ table || out->view_table[i] ? out->view_list[i] : -1, 0, &bad))) goto refused;
+ }
+ for (uint32_t i = 0; i < out->handlers; ++i) {
+ bad = i;
+ if ((why = verify_block(out, out->handler[i].code, -1, -1, out->handler[i].event, &bad))) goto refused;
+ }
+ return PURE_OK;
+refused:
+ if (error_size) snprintf(error, error_size, "%s (%u)", why, bad);
+ pure_module_release(out);
+ return result;
+#undef REFUSE
+}
+
+static int find(const char (*names)[PURE_MODULE_NAME], unsigned count, const char *name) {
+ for (unsigned i = 0; i < count; ++i)
+ if (!strcmp(names[i], name)) return (int)i;
+ return -1;
+}
+
+int pure_module_state(const PureModule *m, const char *name) { return find(m->state_names, m->states, name); }
+int pure_module_input(const PureModule *m, const char *name) { return find(m->input_names, m->inputs, name); }
+int pure_module_view(const PureModule *m, const char *name) {
+ for (uint32_t i = 0; i < m->views; ++i)
+ if (!m->view_table[i] && !strcmp(m->view_names[i], name)) return (int)i;
+ return -1;
+}
+
+int pure_module_position(const PureModule *m, uint32_t view) {
+ if (view >= m->views || m->view_type[view] != PURE_VIEW_NUM || m->view_table[view]) return -1;
+ const PureBlock *block = &m->view_code[view];
+ const PureOp *code = &m->code[block->start];
+ return block->length == 2 && code[0].op == PURE_OP_INDEX_LOAD && code[1].op == PURE_OP_INDEX_NUM ? (int)code[0].arg : -1;
+}
+
+int pure_module_text_state(const PureModule *m, uint32_t view) {
+ if (view >= m->views || m->view_type[view] != PURE_VIEW_TEXT || m->view_table[view]) return -1;
+ const PureBlock *block = &m->view_code[view];
+ return block->length == 1 && m->code[block->start].op == PURE_OP_TEXT_LOAD ? (int)m->code[block->start].arg : -1;
+}
+
+int pure_module_column(const PureModule *m, uint32_t table, const char *name) {
+ for (uint32_t i = 0; i < m->views; ++i)
+ if (m->view_table[i] == table + 1 && !strcmp(m->view_names[i], name)) return (int)i;
+ return -1;
+}
+
+/* ---- Execution ------------------------------------------------------ */
+
+/* IEEE 754-2019 minimum/maximum on finite values: -0 orders below +0. */
+static double minimum(double a, double b) { return a < b || (a == b && signbit(a)) ? a : b; }
+static double maximum(double a, double b) { return a > b || (a == b && !signbit(a)) ? a : b; }
+
+typedef struct { double num; PureBytes bytes; } Slot;
+typedef struct {
+ PureApp *app;
+ const PureInputShape *shape; /* message, or NULL */
+ const char *key; /* a keyed message's key */
+ int64_t base; /* the revision a message's edit was made against, or -1 */
+ PureArena *arena;
+ double dt;
+ size_t row; /* the row a column is evaluated for */
+ bool modified, exists;
+ uint32_t (*spans)[2]; /* a message's variable fields: start and size in the payload */
+} Event;
+
+static PureResult fault(PureApp *app, PureFault kind, uint32_t pc) {
+ app->fault = kind;
+ app->fault_pc = pc;
+ return kind == PURE_FAULT_LIMIT ? PURE_LIMIT : kind == PURE_FAULT_NOT_FOUND ? PURE_NOT_FOUND :
+ kind == PURE_FAULT_EXISTS ? PURE_CONFLICT : PURE_INVALID;
+}
+
+static bool reserve(PureArena *arena, size_t more) {
+ if (more <= arena->capacity - arena->used) return true;
+ size_t capacity = arena->capacity ? arena->capacity : 256;
+ while (capacity - arena->used < more) {
+ if (capacity > SIZE_MAX / 2) return false;
+ capacity *= 2;
+ }
+ unsigned char *bytes = realloc(arena->bytes, capacity);
+ if (!bytes) return false;
+ arena->bytes = bytes;
+ arena->capacity = capacity;
+ return true;
+}
+
+/* Visit a value's bytes in order, from wherever they live. */
+static PureResult visit_value(const PureApp *app, const PureArena *arena, PureBytes v,
+ bool (*visit)(void *, PureSpan), void *context) {
+ if (!v.length) return PURE_OK;
+ const unsigned char *direct = NULL;
+ switch (v.source) {
+ case FROM_PAYLOAD: direct = app->payload.data + v.offset; break;
+ case FROM_COPY: direct = app->copied + v.offset; break;
+ case FROM_CONSTANT: direct = app->module->constant_data + v.offset; break;
+ case FROM_ARENA: direct = arena->bytes + v.offset; break;
+ case FROM_TEXT:
+ return pure_draft_read_version(app->texts[v.owner], v.version, (size_t)v.offset, (size_t)v.length,
+ visit, context);
+ default:
+ return app->data.read(app->data.context, app->document, v.version, (size_t)v.offset,
+ (size_t)v.length, visit, context);
+ }
+ visit(context, (PureSpan){direct, (size_t)v.length});
+ return PURE_OK;
+}
+
+static bool append_span(void *context, PureSpan span) {
+ PureArena *arena = context;
+ memmove(arena->bytes + arena->used, span.data, span.size);
+ arena->used += span.size;
+ return true;
+}
+
+/* Copy a value to the end of the arena. Room is reserved first, so reading
+ * an arena value while appending never follows a moved buffer. */
+static PureResult append_value(const PureApp *app, PureArena *arena, PureBytes v) {
+ if (!reserve(arena, (size_t)v.length)) return PURE_ALLOCATION;
+ return visit_value(app, arena, v, append_span, arena);
+}
+
+/* A contiguous span of a value, copying into the arena when it is spread out. */
+static PureResult contiguous(const PureApp *app, PureArena *arena, PureBytes v, PureSpan *out) {
+ if (v.source == FROM_PAYLOAD || v.source == FROM_COPY || v.source == FROM_CONSTANT) {
+ const unsigned char *base = v.source == FROM_PAYLOAD ? app->payload.data :
+ v.source == FROM_COPY ? app->copied : app->module->constant_data;
+ *out = (PureSpan){v.length ? base + v.offset : NULL, (size_t)v.length};
+ return PURE_OK;
+ }
+ if (v.source == FROM_ARENA) {
+ *out = (PureSpan){arena->bytes + v.offset, (size_t)v.length};
+ return PURE_OK;
+ }
+ size_t start = arena->used;
+ PureResult result = append_value(app, arena, v);
+ *out = (PureSpan){arena->bytes + start, (size_t)v.length};
+ return result;
+}
+
+typedef struct { size_t count; unsigned unit; unsigned utf8; bool valid; } Measure;
+
+static bool measure(void *context, PureSpan span) {
+ Measure *m = context;
+ if (m->unit == 2) m->valid = m->valid && pure_utf8_step(&m->utf8, span);
+ else m->count += m->unit ? pure_utf8_newlines(span) : pure_utf8_scalars(span);
+ return true;
+}
+
+static PureBytes text_of(const PureApp *app, uint32_t state) {
+ PureVersion version = pure_draft_version(app->texts[state]);
+ return (PureBytes){FROM_TEXT, (uint16_t)state, version, 0, version.bytes};
+}
+
+static void move_cursors(const PureModule *m, double *values, uint32_t text, size_t at,
+ size_t removed, size_t inserted) {
+ for (uint32_t c = 0; c < m->states; ++c) {
+ if (m->state_type[c] != PURE_STATE_CURSOR || m->state_ref[c] != text) continue;
+ double position = values[c];
+ if (removed) {
+ if (position >= at + removed) position -= (double)removed;
+ else if (position > at) position = (double)at;
+ } else if (position > at || (position == at && !(m->state_flags[c] & PURE_CURSOR_STAYS)))
+ position += (double)inserted;
+ values[c] = position;
+ }
+}
+
+/* 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];
+}
+
+/* Append bytes to the arena as a value of it. */
+static PureResult keep_bytes(PureArena *arena, const void *bytes, size_t length, PureBytes *out) {
+ if (!reserve(arena, length)) return PURE_ALLOCATION;
+ if (length) memcpy(arena->bytes + arena->used, bytes, length);
+ *out = (PureBytes){FROM_ARENA, 0, {0, 0}, arena->used, length};
+ arena->used += length;
+ return PURE_OK;
+}
+
+/* Run a verified block. Types and stack bounds were proven at load. A change
+ * the data refuses (a size limit, a missing item) fails the event with its
+ * fault; other failures pass through. */
+typedef struct { unsigned char *at; } Copy;
+
+static bool copy_out(void *context, PureSpan span) {
+ Copy *copy = context;
+ if (span.size) memcpy(copy->at, span.data, span.size);
+ copy->at += span.size;
+ return true;
+}
+
+/* An output's message from its field values, laid out as a message's
+ * payload (size bytes), numbered and kept with the event's changes. */
+static PureResult send(PureApp *app, uint32_t output, const Slot *fields, const PureArena *arena, size_t size) {
+ const PureModule *m = app->module;
+ const PureInputShape *shape = &m->input[output];
+ if (app->sent_count == app->sent_capacity) {
+ size_t capacity = app->sent_capacity ? 2 * app->sent_capacity : 16;
+ struct PureSent *grown = realloc(app->sent, capacity * sizeof(*grown));
+ if (!grown) return PURE_ALLOCATION;
+ app->sent = grown;
+ app->sent_capacity = capacity;
+ }
+ unsigned char *payload = malloc(size ? size : 1);
+ if (!payload) return PURE_ALLOCATION;
+ Copy copy = {payload + shape->prefix};
+ for (uint32_t f = 0; f < shape->field_count; ++f) {
+ const PureField *field = &m->field[shape->field_start + f];
+ if (field->type == PURE_FIELD_U32) write32(payload + field->offset, (uint32_t)fields[f].num);
+ else if (field->type == PURE_FIELD_NUMBER) {
+ uint64_t bits;
+ memcpy(&bits, &fields[f].num, 8);
+ write32(payload + field->offset, (uint32_t)bits);
+ write32(payload + field->offset + 4, (uint32_t)(bits >> 32));
+ } else {
+ if (field->offset != PURE_FIELD_REST) write32(payload + field->offset, (uint32_t)fields[f].bytes.length);
+ PureResult result = visit_value(app, arena, fields[f].bytes, copy_out, &copy);
+ if (result != PURE_OK) { free(payload); return result; }
+ }
+ }
+ app->sent[app->sent_count++] = (struct PureSent){output, ++app->last_sent[output], payload, size, false};
+ app->links_changed = true;
+ return PURE_OK;
+}
+
+static PureResult run(const PureModule *m, double *values, PureBlock block, Event *ev, Slot *result) {
+ Slot stack[PURE_MODULE_STACK];
+ size_t sp = 0;
+ PureApp *app = ev->app;
+ PureArena *arena = ev->arena;
+ PureResult status;
+ PureFault kind;
+ const PureOp *op = NULL;
+ for (uint32_t pc = block.start, end = block.start + block.length; pc < end;) {
+ op = &m->code[pc++];
+ uint32_t arg = op->arg;
+ Slot *top = sp ? &stack[sp - 1] : stack;
+ switch (op->op) {
+ case PURE_OP_CONST: stack[sp++].num = op->value; continue;
+ case PURE_OP_LOAD: stack[sp++].num = values[arg]; continue;
+ case PURE_OP_DT: stack[sp++].num = ev->dt; continue;
+ case PURE_OP_NOW: stack[sp++].num = app->now; continue;
+ case PURE_OP_FLOOR: top->num = floor(top->num); continue;
+ case PURE_OP_STORE: case PURE_OP_CUR_STORE: values[arg] = stack[--sp].num; continue;
+ case PURE_OP_JZ: if (stack[--sp].num == 0) pc = arg; continue;
+ case PURE_OP_JMP: pc = arg; continue;
+ case PURE_OP_NEG: top->num = -top->num; continue;
+ case PURE_OP_NOT: top->num = top->num == 0; continue;
+ case PURE_OP_CLAMP: {
+ double high = stack[--sp].num, low = stack[--sp].num;
+ if (low > high) { kind = PURE_FAULT_CLAMP; goto failed; }
+ stack[sp - 1].num = maximum(low, minimum(stack[sp - 1].num, high));
+ continue;
+ }
+ case PURE_OP_FIELD: {
+ const PureField *field = &m->field[ev->shape->field_start + arg];
+ if (field->type == PURE_FIELD_U32) stack[sp++].num = read32(app->payload.data + field->offset);
+ else if (field->type == PURE_FIELD_NUMBER) stack[sp++].num = read_double(app->payload.data + field->offset);
+ else stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, 0, {0, 0}, ev->spans[arg][0], ev->spans[arg][1]};
+ continue;
+ }
+ case PURE_OP_DOC: case PURE_OP_DOC_SIZE: {
+ if (op->op == PURE_OP_DOC && !ev->exists) { kind = PURE_FAULT_NOT_FOUND; goto failed; }
+ PureVersion version = app->data.version(app->data.context, app->document);
+ if (op->op == PURE_OP_DOC) stack[sp++].bytes = (PureBytes){FROM_DOCUMENT, 0, version, 0, version.bytes};
+ else stack[sp++].num = (double)version.bytes;
+ continue;
+ }
+ case PURE_OP_LEN: case PURE_OP_TEXT_BYTES: top->num = (double)top->bytes.length; continue;
+ case PURE_OP_REPLACE: {
+ PureBytes insert = stack[--sp].bytes;
+ double last = stack[--sp].num, first = stack[--sp].num;
+ double size = (double)app->data.version(app->data.context, app->document).bytes;
+ if (first != floor(first) || last != floor(last) || first < 0 || last < first || last > size) {
+ kind = PURE_FAULT_LIMIT;
+ goto failed;
+ }
+ PureSpan span;
+ size_t mark = arena->used;
+ if ((status = contiguous(app, arena, insert, &span)) == PURE_OK)
+ status = app->data.splice(app->data.context, app->document, (size_t)first, (size_t)(last - first), span);
+ arena->used = mark;
+ if (status != PURE_OK) goto refused;
+ ev->modified = ev->exists = true;
+ continue;
+ }
+ /* A reply outlives the arena mark of later statements: keep it by value. */
+ case PURE_OP_REPLY: app->reply = stack[--sp].bytes; app->has_reply = true; continue;
+ case PURE_OP_REQUIRE: if (stack[--sp].num == 0) { kind = PURE_FAULT_LIMIT; goto failed; } continue;
+ case PURE_OP_TEXT_CONST:
+ stack[sp++].bytes = (PureBytes){FROM_CONSTANT, 0, {0, 0}, m->constant_offset[arg], m->constant_length[arg]};
+ continue;
+ case PURE_OP_TEXT_LOAD: stack[sp++].bytes = text_of(app, arg); continue;
+ case PURE_OP_TEXT_INPUT: stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, 0, {0, 0}, 0, app->payload.size}; continue;
+ case PURE_OP_NUM_TEXT: {
+ char digits[32];
+ if ((status = keep_bytes(arena, digits, pure_format_number(top->num, digits), &top->bytes)) != PURE_OK)
+ return status;
+ continue;
+ }
+ case PURE_OP_CONCAT: {
+ PureBytes right = stack[--sp].bytes, left = stack[sp - 1].bytes;
+ size_t start = arena->used;
+ if ((status = append_value(app, arena, left)) != PURE_OK ||
+ (status = append_value(app, arena, right)) != PURE_OK) return status;
+ stack[sp - 1].bytes = (PureBytes){FROM_ARENA, 0, {0, 0}, start, left.length + right.length};
+ continue;
+ }
+ case PURE_OP_CHARS: case PURE_OP_LINES: case PURE_OP_TEXT_FROM: {
+ Measure count = {0, op->op == PURE_OP_LINES ? 1 : op->op == PURE_OP_TEXT_FROM ? 2 : 0, 0, true};
+ if ((status = visit_value(app, arena, top->bytes, measure, &count)) != PURE_OK) return status;
+ if (op->op != PURE_OP_TEXT_FROM) top->num = (double)(count.count + (op->op == PURE_OP_LINES));
+ else if (!count.valid || !pure_utf8_complete(count.utf8)) { kind = PURE_FAULT_ENCODING; goto failed; }
+ continue;
+ }
+ /* Positions: a cursor or an index, with the text or list it is in. */
+ case PURE_OP_CUR_LOAD: case PURE_OP_INDEX_LOAD:
+ stack[sp].num = values[arg];
+ stack[sp++].bytes.owner = m->state_ref[arg];
+ continue;
+ case PURE_OP_CUR_START: case PURE_OP_CUR_END: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST:
+ case PURE_OP_ROW: {
+ size_t last = op->op == PURE_OP_CUR_END ? pure_draft_size(app->texts[arg]) :
+ op->op == PURE_OP_INDEX_LAST ? pure_list_count(app->lists[arg]) : 0;
+ stack[sp].num = op->op == PURE_OP_ROW ? (double)ev->row :
+ op->op == PURE_OP_INDEX_LAST && last ? (double)(last - 1) : (double)last;
+ stack[sp++].bytes.owner = (uint16_t)arg;
+ continue;
+ }
+ case PURE_OP_CUR_MOVE: case PURE_OP_INDEX_MOVE: {
+ double count = stack[--sp].num;
+ Slot *at = &stack[sp - 1];
+ if (count != floor(count)) { kind = PURE_FAULT_LIMIT; goto failed; }
+ if (op->op == PURE_OP_INDEX_MOVE) {
+ size_t items = pure_list_count(app->lists[at->bytes.owner]);
+ at->num = maximum(0, minimum(at->num + count, items ? (double)(items - 1) : 0));
+ continue;
+ }
+ PureDraft *text = app->texts[at->bytes.owner];
+ PureVersion version = pure_draft_version(text);
+ at->num = (double)(arg == PURE_UNIT_CHAR ? pure_text_step(text, version, (size_t)at->num, count)
+ : pure_text_line_step(text, version, (size_t)at->num, count));
+ continue;
+ }
+ case PURE_OP_CUR_COUNT: {
+ PureDraft *text = app->texts[top->bytes.owner];
+ top->num = (double)pure_text_count(text, pure_draft_version(text), (size_t)top->num, arg);
+ continue;
+ }
+ case PURE_OP_TEXT_INSERT: case PURE_OP_TEXT_SET: {
+ PureBytes insert = stack[--sp].bytes;
+ size_t at = op->op == PURE_OP_TEXT_INSERT ? (size_t)stack[--sp].num : 0;
+ size_t removed = op->op == PURE_OP_TEXT_SET ? pure_draft_size(app->texts[arg]) : 0;
+ PureSpan span;
+ size_t mark = arena->used;
+ if ((status = contiguous(app, arena, insert, &span)) == PURE_OK)
+ status = pure_draft_splice(app->texts[arg], at, removed, span);
+ arena->used = mark;
+ if (status != PURE_OK) goto refused;
+ if (op->op == PURE_OP_TEXT_SET) {
+ for (uint32_t c = 0; c < m->states; ++c)
+ if (m->state_type[c] == PURE_STATE_CURSOR && m->state_ref[c] == arg) values[c] = 0;
+ } else move_cursors(m, values, arg, at, 0, span.size);
+ continue;
+ }
+ case PURE_OP_TEXT_REMOVE: {
+ double y = stack[--sp].num, x = stack[--sp].num;
+ size_t first = (size_t)minimum(x, y), last = (size_t)maximum(x, y);
+ if ((status = pure_draft_splice(app->texts[arg], first, last - first, (PureSpan){0})) != PURE_OK) goto refused;
+ if (last > first) move_cursors(m, values, arg, first, last - first, 0);
+ continue;
+ }
+ case PURE_OP_TEXT_LINE: {
+ PureDraft *text = app->texts[arg];
+ PureVersion version = pure_draft_version(text);
+ size_t first = pure_text_line_start(text, version, (size_t)top->num);
+ size_t last = pure_text_line_end(text, version, (size_t)top->num);
+ top->bytes = (PureBytes){FROM_TEXT, (uint16_t)arg, version, first, last - first};
+ continue;
+ }
+ case PURE_OP_BYTES_FROM: case PURE_OP_INDEX_NUM: continue; /* the same bytes or position, retyped */
+ case PURE_OP_LIST_COUNT: stack[sp++].num = (double)pure_list_count(app->lists[arg]); continue;
+ case PURE_OP_SEND: {
+ const PureInputShape *output = &m->input[arg];
+ size_t base = sp - output->field_count, size = output->prefix;
+ sp = base;
+ for (uint32_t f = 0; f < output->field_count; ++f) {
+ uint8_t type = m->field[output->field_start + f].type;
+ double number = stack[base + f].num;
+ if (type == PURE_FIELD_BYTES || type == PURE_FIELD_TEXT) size += (size_t)stack[base + f].bytes.length;
+ else if (type == PURE_FIELD_U32 && !(number >= 0 && number <= UINT32_MAX && number == floor(number))) {
+ kind = PURE_FAULT_LIMIT;
+ goto failed;
+ }
+ }
+ if (size > UINT32_MAX) { kind = PURE_FAULT_LIMIT; goto failed; }
+ if ((status = send(app, arg, stack + base, arena, size)) != PURE_OK) return status;
+ continue;
+ }
+ case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: {
+ PureList *list = app->lists[arg];
+ const PureRecord *record = &m->record[m->state_ref[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) {
+ kind = PURE_FAULT_NOT_FOUND;
+ goto failed;
+ }
+ /* Room for every text first, so no span moves while the next is copied. */
+ size_t mark = arena->used, total = 0;
+ for (unsigned f = 0; f < record->count; ++f)
+ if (text_field(m, arg, f)) total += (size_t)stack[base + f].bytes.length;
+ if (!reserve(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, arg, f) && (status = contiguous(app, arena, stack[base + f].bytes, &span)) != PURE_OK)
+ return status;
+ cells[f] = (PureCell){stack[base + f].num, (unsigned char *)span.data, span.size};
+ }
+ if (m->state_key[arg]) { /* a keyed list places the item by its key */
+ bool found;
+ unsigned key = m->state_key[arg] - 1u;
+ at = pure_list_seek(list, key, cells[key], &found);
+ if (found) { arena->used = mark; kind = PURE_FAULT_EXISTS; goto failed; }
+ }
+ status = pure_list_insert(list, at, cells, &app->undo);
+ arena->used = mark;
+ if (status != PURE_OK) goto refused;
+ move_indices(m, values, arg, pure_list_count(list), at, true);
+ continue;
+ }
+ case PURE_OP_LIST_REMOVE: {
+ size_t at = (size_t)stack[--sp].num;
+ if ((status = pure_list_remove(app->lists[arg], at, &app->undo)) != PURE_OK) goto refused;
+ move_indices(m, values, arg, pure_list_count(app->lists[arg]), at, false);
+ continue;
+ }
+ case PURE_OP_ITEM_GET: {
+ uint32_t list = list_of(arg), field = item_field(arg);
+ size_t at = (size_t)top->num;
+ if (at >= pure_list_count(app->lists[list])) { kind = PURE_FAULT_NOT_FOUND; goto failed; }
+ PureCell cell = pure_list_get(app->lists[list], at, field);
+ /* A text is copied: the item may change or go before the value is used. */
+ if (!text_field(m, list, field)) top->num = cell.num;
+ else if ((status = keep_bytes(arena, cell.text, cell.size, &top->bytes)) != PURE_OK) return status;
+ continue;
+ }
+ case PURE_OP_ITEM_SET: {
+ uint32_t list = list_of(arg), field = item_field(arg);
+ Slot value = stack[--sp];
+ size_t at = (size_t)stack[--sp].num, mark = arena->used;
+ PureSpan span = {0};
+ status = text_field(m, list, field) ? contiguous(app, arena, value.bytes, &span) : PURE_OK;
+ if (status == PURE_OK)
+ status = pure_list_set(app->lists[list], at, field,
+ (PureCell){value.num, (unsigned char *)span.data, span.size}, &app->undo);
+ arena->used = mark;
+ if (status != PURE_OK) goto refused;
+ 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[arg]])) {
+ kind = PURE_FAULT_NOT_FOUND;
+ goto failed;
+ }
+ values[arg] = position;
+ continue;
+ }
+ case PURE_OP_FOLD: {
+ /* The block that follows runs once per item, as a column of the list. */
+ unsigned how = arg & 0xFF;
+ PureBlock inner = {pc, arg >> 16};
+ PureList *list = app->lists[arg >> 8 & 0xFF];
+ pc += inner.length;
+ double total = 0;
+ bool any = false;
+ Event per = *ev;
+ for (size_t row = 0, items = pure_list_count(list); row < items; ++row) {
+ Slot out[2];
+ size_t mark = arena->used;
+ per.row = row;
+ status = run(m, values, inner, &per, out);
+ arena->used = mark;
+ if (status != PURE_OK) return status;
+ double value = out[0].num;
+ if (how == PURE_FOLD_COUNT || how == PURE_FOLD_FIRST ? value == 0 : out[1].num == 0) continue;
+ if (how == PURE_FOLD_FIRST) { total = (double)row; any = true; break; }
+ total = how == PURE_FOLD_COUNT ? total + 1 : how == PURE_FOLD_SUM ? total + value :
+ !any ? value : how == PURE_FOLD_MAX ? maximum(total, value) : minimum(total, value);
+ any = true;
+ }
+ if (!any && how >= PURE_FOLD_MAX) { kind = PURE_FAULT_NOT_FOUND; goto failed; }
+ if (!isfinite(total)) { kind = PURE_FAULT_NON_FINITE; goto failed; }
+ stack[sp].num = total;
+ stack[sp++].bytes.owner = (uint16_t)(arg >> 8 & 0xFF); /* first: the list the index is in */
+ continue;
+ }
+ case PURE_OP_TEXT_COMPARE: {
+ /* Texts order by their bytes: a shorter text before a longer one it begins. */
+ PureBytes right = stack[--sp].bytes, left = stack[sp - 1].bytes;
+ size_t mark = arena->used;
+ PureSpan a, b;
+ if (!reserve(arena, (size_t)(left.length + right.length))) return PURE_ALLOCATION;
+ if ((status = contiguous(app, arena, left, &a)) != PURE_OK ||
+ (status = contiguous(app, arena, right, &b)) != PURE_OK) return status;
+ size_t shorter = a.size < b.size ? a.size : b.size;
+ int order = shorter ? memcmp(a.data, b.data, shorter) : 0;
+ if (!order) order = (a.size > b.size) - (a.size < b.size);
+ arena->used = mark;
+ stack[sp - 1].num = arg == PURE_LESS ? order < 0 : arg == PURE_LESS_EQUAL ? order <= 0 :
+ arg == PURE_GREATER ? order > 0 : arg == PURE_GREATER_EQUAL ? order >= 0 :
+ arg == PURE_EQUAL ? order == 0 : order != 0;
+ continue;
+ }
+ case PURE_OP_LIST_FIND: case PURE_OP_LIST_HAS: {
+ /* A keyed list's item by key, found by halving: FIND gives its index, HAS whether it is there. */
+ unsigned key = m->state_key[arg] - 1u;
+ size_t mark = arena->used;
+ PureCell cell = {top->num, NULL, 0};
+ if (text_field(m, arg, key)) {
+ PureSpan span;
+ if ((status = contiguous(app, arena, top->bytes, &span)) != PURE_OK) return status;
+ cell = (PureCell){0, (unsigned char *)span.data, span.size};
+ }
+ bool found;
+ size_t at = pure_list_seek(app->lists[arg], key, cell, &found);
+ arena->used = mark;
+ if (op->op == PURE_OP_LIST_HAS) { top->num = found; continue; }
+ if (!found) { kind = PURE_FAULT_NOT_FOUND; goto failed; }
+ top->num = (double)at;
+ top->bytes.owner = (uint16_t)arg;
+ continue;
+ }
+ case PURE_OP_PICK: stack[sp] = stack[sp - 1 - arg]; ++sp; continue;
+ case PURE_OP_SLIDE: stack[sp - 1 - arg] = stack[sp - 1]; sp -= arg; continue;
+ default: break;
+ }
+ double b = stack[--sp].num, a = stack[sp - 1].num, value;
+ switch (op->op) {
+ case PURE_OP_ADD: value = a + b; break;
+ case PURE_OP_SUB: value = a - b; break;
+ case PURE_OP_MUL: value = a * b; break;
+ case PURE_OP_DIV:
+ if (b == 0) { kind = PURE_FAULT_DIVIDE; goto failed; }
+ value = a / b;
+ break;
+ case PURE_OP_LT: value = a < b; break;
+ case PURE_OP_LE: value = a <= b; break;
+ case PURE_OP_GT: value = a > b; break;
+ case PURE_OP_GE: value = a >= b; break;
+ case PURE_OP_EQ: value = a == b; break;
+ case PURE_OP_NE: value = a != b; break;
+ case PURE_OP_AND: value = a != 0 && b != 0; break;
+ case PURE_OP_OR: value = a != 0 || b != 0; break;
+ case PURE_OP_MIN: value = minimum(a, b); break;
+ case PURE_OP_MOD: /* the divisor's sign, as floor division leaves it (a zero too) */
+ if (b == 0) { kind = PURE_FAULT_DIVIDE; goto failed; }
+ value = fmod(a, b);
+ if (value != 0 && (value < 0) != (b < 0)) value += b;
+ else if (value == 0) value = copysign(0.0, b);
+ break;
+ default: value = maximum(a, b); break;
+ }
+ if (!isfinite(value)) { kind = PURE_FAULT_NON_FINITE; goto failed; }
+ stack[sp - 1].num = value;
+ }
+ if (result) memcpy(result, stack, sp * sizeof(*stack)); /* a view's value, or a fold block's numbers */
+ return PURE_OK;
+refused:
+ kind = status == PURE_LIMIT ? PURE_FAULT_LIMIT : status == PURE_NOT_FOUND ? PURE_FAULT_NOT_FOUND : PURE_FAULT_NONE;
+ if (kind == PURE_FAULT_NONE) return status;
+failed:
+ return fault(app, kind, (uint32_t)(op - m->code));
+}
+
+PURE_COLD static PureResult new_text(PureDraft **out, const PureModule *m, uint32_t constant) {
+ *out = pure_draft_new();
+ if (!*out) return PURE_ALLOCATION;
+ PureSpan initial = {m->constant_data + m->constant_offset[constant], m->constant_length[constant]};
+ PureResult result = pure_draft_splice(*out, 0, 0, initial);
+ if (result != PURE_OK) { pure_draft_close(*out); *out = NULL; }
+ return result;
+}
+
+PURE_COLD 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);
+}
+
+static bool pending(const PureApp *app, const void *document) {
+ for (unsigned i = 0; i < app->pending_count; ++i) if (app->pending[i].document == document) return true;
+ return false;
+}
+
+void pure_app_finish(PureApp *app) {
+ /* A document in the unsaved burst stays open until the save. */
+ if (app->document && !pending(app, app->document)) app->data.close(app->data.context, app->document);
+ free(app->copied);
+ app->document = NULL;
+ app->copied = NULL;
+ app->has_reply = false;
+ app->payload = (PureSpan){0};
+ app->arena.used = 0;
+}
+
+/* The start of a burst: what a failed save returns to. Text and stored list
+ * drafts stay held until the save, so every version since stays readable. */
+static void open_burst(PureApp *app) {
+ if (app->open) return;
+ const PureModule *m = app->module;
+ memcpy(app->durable_values, app->values, sizeof(app->values));
+ app->durable_ticks = app->ticks;
+ app->durable_events = app->events;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (app->texts[i]) {
+ pure_draft_hold(app->texts[i], true);
+ app->durable_texts[i] = pure_draft_version(app->texts[i]);
+ }
+ if (app->list_drafts[i]) {
+ pure_draft_hold(app->list_drafts[i], true);
+ app->durable_lists[i] = pure_draft_version(app->list_drafts[i]);
+ }
+ }
+ app->open = true;
+}
+
+/* End a burst, keeping it (saved) or undoing all of it. The documents it held
+ * close, except the current event's, which pure_app_finish closes. */
+static void close_burst(PureApp *app, bool keep) {
+ for (unsigned i = 0; i < app->pending_count; ++i)
+ if (app->pending[i].document != app->document) app->data.close(app->data.context, app->pending[i].document);
+ app->pending_count = 0;
+ if (!app->open) return;
+ const PureModule *m = app->module;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (app->texts[i]) {
+ if (!keep) pure_draft_restore(app->texts[i], app->durable_texts[i]);
+ pure_draft_hold(app->texts[i], false);
+ }
+ if (app->list_drafts[i]) {
+ if (!keep) pure_draft_restore(app->list_drafts[i], app->durable_lists[i]);
+ pure_draft_hold(app->list_drafts[i], false);
+ }
+ }
+ if (keep) pure_undo_commit(&app->undo);
+ else {
+ pure_undo_rollback(&app->undo, 0);
+ memcpy(app->values, app->durable_values, sizeof(app->values));
+ app->ticks = app->durable_ticks;
+ app->events = app->durable_events;
+ ++app->changes;
+ }
+ app->open = false;
+}
+
+static void drop_sent(PureApp *app, size_t count) {
+ while (app->sent_count > count) free(app->sent[--app->sent_count].payload);
+}
+
+/* After a save: what it stored is the links' saved state (trimmed sends go),
+ * or, when it failed, the links return to the last saved state. */
+static void settle_links(PureApp *app, bool saved) {
+ if (saved) {
+ size_t kept = 0;
+ for (size_t i = 0; i < app->sent_count; ++i) {
+ if (app->sent[i].trimmed) free(app->sent[i].payload);
+ else app->sent[kept++] = app->sent[i];
+ }
+ app->sent_count = app->sent_durable = kept;
+ memcpy(app->saved_sent, app->last_sent, sizeof(app->saved_sent));
+ for (unsigned i = 0; i < app->heard_count; ++i) app->heard[i].saved = app->heard[i].seq;
+ } else {
+ drop_sent(app, app->sent_durable);
+ for (size_t i = 0; i < app->sent_count; ++i) app->sent[i].trimmed = false;
+ memcpy(app->last_sent, app->saved_sent, sizeof(app->last_sent));
+ unsigned kept = 0;
+ for (unsigned i = 0; i < app->heard_count; ++i)
+ if (app->heard[i].saved) {
+ app->heard[kept] = app->heard[i];
+ app->heard[kept++].seq = app->heard[i].saved;
+ }
+ app->heard_count = kept;
+ }
+ app->links_changed = false;
+}
+
+PureResult pure_app_save(PureApp *app) {
+ void *documents[PURE_APP_PENDING];
+ int64_t revisions[PURE_APP_PENDING];
+ size_t count = 0;
+ for (unsigned i = 0; i < app->pending_count; ++i)
+ if (app->pending[i].edited) documents[count++] = app->pending[i].document;
+ PureResult result = pure_save_commit(app, app->values, documents, count, revisions);
+ settle_links(app, result == PURE_OK);
+ close_burst(app, result == PURE_OK);
+ return result;
+}
+
+bool pure_app_defer(PureApp *app, bool deferred) {
+ if (deferred && !pure_save_enabled(app)) return false;
+ app->deferred = deferred;
+ return true;
+}
+
+/* ---- Snapshots. Snapshots of one app form a chain from oldest to newest,
+ * each holding the next, so they go oldest first. List items the app lets go
+ * while a snapshot is its newest may be read by that one and older ones, so
+ * they go with it. */
+struct PureSnapshot {
+ unsigned refs;
+ const PureModule *module;
+ uint64_t changes;
+ double values[PURE_MODULE_STATES];
+ PureDraft *texts[PURE_MODULE_STATES];
+ PureList *lists[PURE_MODULE_STATES];
+ PureRetired retired;
+ struct PureSnapshot *newer;
+};
+
+void pure_snapshot_hold(PureSnapshot *snapshot) { __atomic_add_fetch(&snapshot->refs, 1, __ATOMIC_RELAXED); }
+
+void pure_snapshot_release(PureSnapshot *snapshot) {
+ while (snapshot && !__atomic_sub_fetch(&snapshot->refs, 1, __ATOMIC_ACQ_REL)) {
+ PureSnapshot *newer = snapshot->newer;
+ for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
+ pure_draft_close(snapshot->texts[i]);
+ pure_list_free(snapshot->lists[i]);
+ }
+ pure_retired_free(&snapshot->retired);
+ free(snapshot);
+ snapshot = newer;
+ }
+}
+
+void pure_app_stop(PureApp *app) {
+ pure_app_finish(app);
+ close_burst(app, true); /* unsaved changes are dropped with the app */
+ for (uint32_t i = 0; i < PURE_MODULE_STATES && !app->reading; ++i) {
+ pure_draft_close(app->texts[i]);
+ pure_list_free(app->lists[i]);
+ pure_draft_close(app->list_drafts[i]);
+ }
+ /* Items the newest snapshot may still read go when it goes. */
+ if (app->snapshot) {
+ app->snapshot->retired = app->retired;
+ pure_snapshot_release(app->snapshot);
+ } else
+ pure_retired_free(&app->retired);
+ pure_draft_close(app->stored_values);
+ drop_sent(app, 0);
+ free(app->sent);
+ free(app->heard);
+ free(app->carried);
+ pure_draft_close(app->stored_links);
+ free(app->pending);
+ pure_undo_free(&app->undo);
+ for (unsigned i = 0; i < app->row_tables; ++i) free(app->rows[i].items);
+ free(app->rows);
+ free(app->arena.bytes);
+ free(app->view_arena.bytes);
+ memset(app, 0, sizeof(*app));
+}
+
+/* Where one event began inside the burst: a failed event returns there. */
+typedef struct {
+ PureVersion texts[PURE_MODULE_STATES], lists[PURE_MODULE_STATES];
+ size_t undo, sent;
+ uint64_t last_sent[PURE_MODULE_INPUTS];
+ bool opened;
+} Mark;
+
+static void mark_event(PureApp *app, Mark *mark) {
+ mark->opened = !app->open;
+ open_burst(app);
+ for (uint32_t i = 0; i < app->module->states; ++i) {
+ if (app->texts[i]) mark->texts[i] = pure_draft_version(app->texts[i]);
+ if (app->list_drafts[i]) mark->lists[i] = pure_draft_version(app->list_drafts[i]);
+ }
+ mark->undo = app->undo.count;
+ mark->sent = app->sent_count;
+ memcpy(mark->last_sent, app->last_sent, sizeof(mark->last_sent));
+}
+
+static void undo_event(PureApp *app, const Mark *mark) {
+ for (uint32_t i = 0; i < app->module->states; ++i) {
+ if (app->texts[i]) pure_draft_restore(app->texts[i], mark->texts[i]);
+ if (app->list_drafts[i]) pure_draft_restore(app->list_drafts[i], mark->lists[i]);
+ }
+ pure_undo_rollback(&app->undo, mark->undo);
+ drop_sent(app, mark->sent);
+ memcpy(app->last_sent, mark->last_sent, sizeof(app->last_sent));
+ if (mark->opened) close_burst(app, true); /* nothing happened: release the holds */
+}
+
+/* The last number heard from source, or NULL. */
+static struct PureHeard *heard_from(PureApp *app, const char *source) {
+ for (unsigned i = 0; i < app->heard_count; ++i)
+ if (!strcmp(app->heard[i].source, source)) return &app->heard[i];
+ return NULL;
+}
+
+/* Hear seq from source (room for a new source was made before). */
+static void hear(PureApp *app, const char *source, uint64_t seq) {
+ struct PureHeard *heard = heard_from(app, source);
+ if (!heard) {
+ heard = &app->heard[app->heard_count++];
+ *heard = (struct PureHeard){.saved = 0};
+ memcpy(heard->source, source, strlen(source) + 1);
+ }
+ heard->seq = seq;
+ app->links_changed = true;
+}
+
+static PureResult keep_reply(PureApp *app);
+
+/* One event: every handler for it runs in module order against pending
+ * states, all or nothing. A key, text or message event saves before it
+ * returns unless saving is deferred; a frame leaves that to the next event
+ * (frames come 60 times a second). A message's document joins the burst. */
+static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
+ const PureModule *m = app->module;
+ double pending[PURE_MODULE_STATES];
+ Mark mark;
+ mark_event(app, &mark);
+ memcpy(pending, app->values, sizeof(app->values));
+ app->fault = PURE_FAULT_NONE;
+ app->fault_pc = UINT32_MAX;
+ PureResult result = PURE_OK;
+ for (uint32_t i = 0; i < m->handlers && result == PURE_OK; ++i)
+ if (m->handler[i].event == event) result = run(m, pending, m->handler[i].code, ev, NULL);
+ /* An edit needs the revision it was made against. A save rebases an
+ * edited document and releasing texts may reclaim earlier text versions:
+ * keep such a reply by copying it first. */
+ if (result == PURE_OK && ev->modified && ev->base < 0) result = PURE_INVALID;
+ if (result == PURE_OK && app->has_reply &&
+ ((app->reply.source == FROM_DOCUMENT && ev->modified) || app->reply.source == FROM_TEXT))
+ result = keep_reply(app);
+ if (result != PURE_OK) {
+ undo_event(app, &mark);
+ return result;
+ }
+ memcpy(app->values, pending, sizeof(pending));
+ ++app->events;
+ ++app->changes;
+ if (!event) ++app->ticks;
+ if (app->document) {
+ app->pending[app->pending_count].document = app->document;
+ app->pending[app->pending_count].edited = ev->modified;
+ memcpy(app->pending[app->pending_count++].key, ev->key, strlen(ev->key) + 1);
+ app->revision += ev->modified; /* a save moves an edited document one revision */
+ }
+ if (app->delivering.pending) {
+ hear(app, app->delivering.source, app->delivering.seq);
+ app->delivering.pending = false;
+ }
+ return event && !app->deferred ? pure_app_save(app) : PURE_OK;
+}
+
+PureResult pure_app_frame(PureApp *app) {
+ pure_app_finish(app);
+ Event ev = {.app = app, .arena = &app->arena, .dt = 1.0 / 60.0};
+ return dispatch(app, 0, &ev);
+}
+
+PureResult pure_app_tick(PureApp *app, uint32_t input) {
+ if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_TIMER) return PURE_INVALID;
+ pure_app_finish(app);
+ Event ev = {.app = app, .arena = &app->arena};
+ return dispatch(app, 1 + 2 * input, &ev);
+}
+
+PureResult pure_app_input(PureApp *app, uint32_t input, PureEdge edge) {
+ if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_KEY ||
+ (edge != PURE_EDGE_DOWN && edge != PURE_EDGE_UP)) return PURE_INVALID;
+ pure_app_finish(app);
+ Event ev = {.app = app, .arena = &app->arena};
+ return dispatch(app, 1 + 2 * input + (edge == PURE_EDGE_UP), &ev);
+}
+
+PureResult pure_app_point(PureApp *app, uint32_t state, size_t position) {
+ const PureModule *m = app->module;
+ pure_app_finish(app);
+ if (state >= m->states || m->state_type[state] != PURE_STATE_INDEX) return PURE_INVALID;
+ if (position >= pure_list_count(app->lists[m->state_ref[state]])) return PURE_NOT_FOUND;
+ open_burst(app); /* what a failed save returns to */
+ app->fault = PURE_FAULT_NONE;
+ app->fault_pc = UINT32_MAX;
+ app->values[state] = (double)position;
+ ++app->events;
+ ++app->changes;
+ return app->deferred ? PURE_OK : pure_app_save(app);
+}
+
+PureResult pure_app_text(PureApp *app, uint32_t input, PureSpan text) {
+ pure_app_finish(app);
+ if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_TEXT ||
+ (text.size && !text.data) || !pure_utf8_valid(text)) return PURE_INVALID;
+ app->payload = text;
+ Event ev = {.app = app, .arena = &app->arena};
+ PureResult result = dispatch(app, 1 + 2 * input, &ev);
+ app->payload = (PureSpan){0};
+ return result;
+}
+
+static PureResult keep_reply(PureApp *app) {
+ size_t length = (size_t)app->reply.length, start = app->arena.used;
+ unsigned char *copy = malloc(length ? length : 1);
+ if (!copy) return PURE_ALLOCATION;
+ PureResult result = append_value(app, &app->arena, app->reply);
+ if (result != PURE_OK) { free(copy); return result; }
+ if (length) memcpy(copy, app->arena.bytes + start, length);
+ app->arena.used = start;
+ app->copied = copy;
+ app->reply = (PureBytes){FROM_COPY, 0, {0, 0}, 0, length};
+ return PURE_OK;
+}
+
+/* A message whose variable fields are where placed says (start and size in
+ * the payload), or, without it, one after another after the prefix. */
+static PureResult message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed) {
+ pure_app_finish(app);
+ const PureModule *m = app->module;
+ app->fault = PURE_FAULT_NONE;
+ app->fault_pc = UINT32_MAX;
+ app->revision = 0;
+ if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || base < -1 ||
+ (payload.size && !payload.data) || (m->input[input].keyed ? !document_key(key) : key != NULL))
+ return PURE_INVALID;
+ const PureInputShape *shape = &m->input[input];
+ if (payload.size < shape->prefix) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
+ /* Where each variable field is; a length past the payload, a text that
+ * is not UTF-8 or a number that is not finite fails the message. */
+ uint32_t spans[PURE_MODULE_FIELDS][2];
+ size_t at = shape->prefix;
+ for (uint32_t f = 0; f < shape->field_count; ++f) {
+ const PureField *field = &m->field[shape->field_start + f];
+ if (field->type == PURE_FIELD_NUMBER && !isfinite(read_double(payload.data + field->offset)))
+ return fault(app, PURE_FAULT_NON_FINITE, UINT32_MAX);
+ if (field->type != PURE_FIELD_BYTES && field->type != PURE_FIELD_TEXT) continue;
+ size_t start = placed ? placed[2 * f] : at;
+ if (start > payload.size) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
+ size_t size = placed ? placed[2 * f + 1] : field->offset == PURE_FIELD_REST ? payload.size - at :
+ read32(payload.data + field->offset);
+ if (size > payload.size - start) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
+ if (field->type == PURE_FIELD_TEXT && !pure_utf8_valid((PureSpan){size ? payload.data + start : NULL, size}))
+ return fault(app, PURE_FAULT_ENCODING, UINT32_MAX);
+ spans[f][0] = (uint32_t)start;
+ spans[f][1] = (uint32_t)size;
+ at = start + size;
+ }
+ if (shape->keyed && m->collections) {
+ if (!app->data.open) return PURE_INVALID;
+ /* A key joins a burst once, so its revision moves once per save. */
+ bool seen = app->pending_count == PURE_APP_PENDING;
+ for (unsigned i = 0; i < app->pending_count && !seen; ++i) seen = !strcmp(app->pending[i].key, key);
+ if (seen) {
+ PureResult saved = pure_app_save(app);
+ if (saved != PURE_OK) return saved;
+ }
+ if (app->pending_count == app->pending_capacity) {
+ unsigned capacity = app->pending_capacity ? 2 * app->pending_capacity : 16;
+ struct PurePending *grown = realloc(app->pending, capacity * sizeof(*grown));
+ if (!grown) return PURE_ALLOCATION;
+ app->pending = grown;
+ app->pending_capacity = capacity;
+ }
+ PureResult opened = app->data.open(app->data.context, 0, key, &app->document, &app->revision);
+ if (opened != PURE_OK) { app->document = NULL; return opened; }
+ if (base >= 0 && base != app->revision) { pure_app_finish(app); return PURE_CONFLICT; }
+ }
+ app->payload = payload;
+ Event ev = {.app = app, .shape = shape, .key = key, .base = base, .arena = &app->arena, .exists = app->revision > 0,
+ .spans = spans};
+ PureResult result = dispatch(app, 1 + 2 * input, &ev);
+ if (result != PURE_OK) pure_app_finish(app);
+ return result;
+}
+
+PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload) {
+ return message(app, input, key, base, payload, NULL);
+}
+
+PureResult pure_app_message_fields(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed) {
+ return message(app, input, key, base, payload, placed);
+}
+
+void pure_app_outbox(const PureApp *app, PureSentVisit visit, void *context) {
+ for (size_t i = 0; i < app->sent_durable; ++i) {
+ const struct PureSent *sent = &app->sent[i];
+ if (!sent->trimmed && !visit(context, sent->output, sent->seq, (PureSpan){sent->payload, sent->size})) return;
+ }
+}
+
+void pure_app_outbox_newest(const PureApp *app, PureSentVisit visit, void *context) {
+ for (size_t i = app->sent_durable; i-- > 0;) {
+ const struct PureSent *sent = &app->sent[i];
+ if (!sent->trimmed && !visit(context, sent->output, sent->seq, (PureSpan){sent->payload, sent->size})) return;
+ }
+}
+
+void pure_app_trim(PureApp *app, uint32_t output, uint64_t through) {
+ for (size_t i = 0; i < app->sent_durable; ++i)
+ if (app->sent[i].output == output && app->sent[i].seq <= through && !app->sent[i].trimmed) {
+ app->sent[i].trimmed = true;
+ app->links_changed = true;
+ }
+}
+
+PureResult pure_app_deliver(PureApp *app, uint32_t input, const char *source, uint64_t seq, PureSpan payload,
+ const uint32_t *placed) {
+ const PureModule *m = app->module;
+ size_t length = source ? strlen(source) : 0;
+ app->duplicate = false;
+ if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || m->input[input].keyed || !length ||
+ length > PURE_SOURCE || !seq) return PURE_INVALID;
+ for (size_t i = 0; i < length; ++i)
+ if (source[i] <= ' ' || source[i] > '~') return PURE_INVALID;
+ struct PureHeard *heard = heard_from(app, source);
+ if (heard && seq <= heard->seq) {
+ pure_app_finish(app);
+ app->duplicate = true;
+ return PURE_OK;
+ }
+ if (!heard && app->heard_count == app->heard_capacity) {
+ unsigned capacity = app->heard_capacity ? 2 * app->heard_capacity : 4;
+ struct PureHeard *grown = realloc(app->heard, capacity * sizeof(*grown));
+ if (!grown) return PURE_ALLOCATION;
+ app->heard = grown;
+ app->heard_capacity = capacity;
+ }
+ app->delivering.source = source;
+ app->delivering.seq = seq;
+ app->delivering.pending = true;
+ PureResult result = message(app, input, NULL, -1, payload, placed);
+ if (!app->delivering.pending) return result;
+ /* Not applied. Refused by the app (a fault), the message was delivered
+ * all the same: its number is heard, saved like a key event's changes. */
+ app->delivering.pending = false;
+ if (app->fault == PURE_FAULT_NONE) return result;
+ Mark mark;
+ mark_event(app, &mark);
+ hear(app, source, seq);
+ PureResult saved = app->deferred ? PURE_OK : pure_app_save(app);
+ return saved == PURE_OK ? result : saved;
+}
+
+PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), void *context) {
+ if (!app->has_reply) return PURE_OK;
+ return visit_value(app, &app->arena, app->reply, visit, context);
+}
+
+/* A view that fails reports it by its result; the fault stays the last event's. */
+static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) {
+ double values[PURE_MODULE_STATES];
+ memcpy(values, app->values, sizeof(values));
+ app->view_arena.used = 0;
+ Event ev = {.app = app, .arena = &app->view_arena, .row = row};
+ PureFault fault = app->fault;
+ uint32_t fault_pc = app->fault_pc;
+ PureResult result = run(app->module, values, app->module->view_code[view], &ev, out);
+ app->fault = fault;
+ app->fault_pc = fault_pc;
+ return result;
+}
+
+/* A filtered table's items, as fresh as the app's states: the filter runs
+ * once per item after each change (an item it fails on is left out), not
+ * once per cell. NULL for a table without a filter, or without room. */
+static struct PureRows *filtered(PureApp *app, uint32_t table) {
+ const PureModule *m = app->module;
+ if (!m->view_code[table].length) return NULL;
+ struct PureRows *rows = NULL;
+ for (unsigned i = 0; i < app->row_tables && !rows; ++i) if (app->rows[i].table == table) rows = &app->rows[i];
+ if (!rows) {
+ struct PureRows *grown = realloc(app->rows, (app->row_tables + 1) * sizeof(*grown));
+ if (!grown) return NULL;
+ app->rows = grown;
+ rows = &app->rows[app->row_tables++];
+ *rows = (struct PureRows){.table = table, .changes = UINT64_MAX};
+ }
+ if (rows->changes == app->changes) return rows;
+ rows->count = 0;
+ for (size_t item = 0, items = pure_list_count(app->lists[m->view_list[table]]); item < items; ++item) {
+ Slot keep;
+ if (evaluate(app, table, item, &keep) != PURE_OK || keep.num == 0) continue;
+ if (rows->count == rows->capacity) {
+ size_t capacity = rows->capacity ? 2 * rows->capacity : 16;
+ size_t *grown = realloc(rows->items, capacity * sizeof(*grown));
+ if (!grown) return NULL;
+ rows->items = grown;
+ rows->capacity = capacity;
+ }
+ rows->items[rows->count++] = item;
+ }
+ rows->changes = app->changes;
+ return rows;
+}
+
+bool pure_app_row_item(PureApp *app, uint32_t table, size_t row, size_t *item) {
+ const PureModule *m = app->module;
+ if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return false;
+ if (!m->view_code[table].length) {
+ *item = row;
+ return row < pure_list_count(app->lists[m->view_list[table]]);
+ }
+ struct PureRows *rows = filtered(app, table);
+ if (!rows || row >= rows->count) return false;
+ *item = rows->items[row];
+ return true;
+}
+
+bool pure_app_item_row(PureApp *app, uint32_t table, size_t item, size_t *row) {
+ const PureModule *m = app->module;
+ if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return false;
+ if (!m->view_code[table].length) {
+ *row = item;
+ return item < pure_list_count(app->lists[m->view_list[table]]);
+ }
+ struct PureRows *rows = filtered(app, table);
+ size_t low = 0, high = rows ? rows->count : 0; /* the items are in order */
+ while (low < high) {
+ size_t middle = low + (high - low) / 2;
+ if (rows->items[middle] < item) low = middle + 1;
+ else high = middle;
+ }
+ if (low == (rows ? rows->count : 0) || rows->items[low] != item) return false;
+ *row = low;
+ return true;
+}
+
+/* A view outside a table (column false), or a column and a row of its table. */
+static PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type, Slot *slot) {
+ const PureModule *m = app->module;
+ if (view >= m->views || m->view_type[view] != type || (m->view_table[view] != 0) != column) return PURE_INVALID;
+ if (column && !pure_app_row_item(app, m->view_table[view] - 1u, row, &row)) return PURE_NOT_FOUND;
+ return evaluate(app, view, row, slot);
+}
+
+bool pure_app_view(PureApp *app, uint32_t view, double *out) {
+ Slot slot;
+ if (view_of(app, view, false, 0, PURE_VIEW_NUM, &slot) != PURE_OK) return false;
+ *out = slot.num;
+ return true;
+}
+
+PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *, PureSpan), void *context) {
+ Slot slot;
+ PureResult result = view_of(app, view, false, 0, PURE_VIEW_TEXT, &slot);
+ return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result;
+}
+
+size_t pure_app_rows(PureApp *app, uint32_t table) {
+ const PureModule *m = app->module;
+ if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return 0;
+ struct PureRows *rows = filtered(app, table);
+ return rows ? rows->count : m->view_code[table].length ? 0 : pure_list_count(app->lists[m->view_list[table]]);
+}
+
+bool pure_app_cell(PureApp *app, uint32_t column, size_t row, double *out) {
+ Slot slot;
+ if (view_of(app, column, true, row, PURE_VIEW_NUM, &slot) != PURE_OK) return false;
+ *out = slot.num;
+ return true;
+}
+
+PureResult pure_app_cell_text(PureApp *app, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context) {
+ Slot slot;
+ PureResult result = view_of(app, column, true, row, PURE_VIEW_TEXT, &slot);
+ return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result;
+}
+
+PureSnapshot *pure_app_snapshot(PureApp *app) {
+ if (app->open || app->reading) return NULL;
+ const PureModule *m = app->module;
+ PureSnapshot *made = calloc(1, sizeof(*made)), *last = app->snapshot;
+ if (!made) return NULL;
+ made->refs = 1;
+ made->module = m;
+ made->changes = app->changes;
+ memcpy(made->values, app->values, sizeof(made->values));
+ bool ok = true;
+ for (uint32_t i = 0; i < m->states && ok; ++i) {
+ if (app->texts[i]) ok = (made->texts[i] = pure_draft_freeze(app->texts[i])) != NULL;
+ if (!ok || !app->lists[i]) continue;
+ if (last && last->lists[i] && pure_list_current(last->lists[i], app->lists[i])) {
+ pure_list_hold(last->lists[i]);
+ made->lists[i] = last->lists[i];
+ } else
+ ok = (made->lists[i] = pure_list_share(app->lists[i], &app->retired)) != NULL;
+ }
+ if (!ok) {
+ pure_snapshot_release(made);
+ return NULL;
+ }
+ made->refs = last ? 3 : 2; /* the app's, the caller's and the last one's */
+ if (last) {
+ last->retired = app->retired;
+ last->newer = made;
+ pure_snapshot_release(last);
+ } else
+ pure_retired_free(&app->retired); /* shared only by a snapshot never made */
+ app->retired = (PureRetired){0};
+ app->snapshot = made;
+ return made;
+}
+
+void pure_app_read(PureApp *reader, const PureSnapshot *snapshot) {
+ reader->module = snapshot->module;
+ reader->reading = true;
+ reader->changes = snapshot->changes; /* what the row cache is fresh for */
+ memcpy(reader->values, snapshot->values, sizeof(reader->values));
+ memcpy(reader->texts, snapshot->texts, sizeof(reader->texts));
+ memcpy(reader->lists, snapshot->lists, sizeof(reader->lists));
+}
+
+/* A list with record's fields: each takes the field of `from` with the same
+ * name and kind, or its default. */
+PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names,
+ const uint8_t *text, unsigned count, const PureModule *m, uint32_t record) {
+ const PureRecord *r = &m->record[record];
+ int source[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < r->count; ++f) {
+ source[f] = -1;
+ for (unsigned k = 0; k < count; ++k)
+ if (!strcmp(m->record_field_names[r->first + f], names[k]) && m->record_field_text[r->first + f] == text[k])
+ source[f] = (int)k;
+ }
+ 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;
+}
+
+/* A kept list keeps its items; with the same record it is the same list. */
+PURE_COLD 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];
+ const char *names[PURE_RECORD_FIELDS];
+ bool same = r->count == o->count;
+ for (unsigned k = 0; k < o->count; ++k) {
+ names[k] = old->record_field_names[o->first + k];
+ same = same && !strcmp(names[k], m->record_field_names[r->first + k]) &&
+ old->record_field_text[o->first + k] == m->record_field_text[r->first + k];
+ }
+ if (same) { *out = from; return PURE_OK; }
+ return pure_list_migrate(out, from, names, old->record_field_text + o->first, o->count, m, record);
+}
+
+/* Open a module in an app. States keep their values by name and kind (texts
+ * their content, cursors their position in it, lists their items, moved by
+ * field name when the record changed); every other state starts from its
+ * stored value, or its initial one. A list whose record changed is stored
+ * again, and the next event or save publishes the module's states; a state
+ * the module drops stays stored as it was. Starting an app opens its module
+ * over none. */
+/* Outputs of old that module has by name and with the same fields keep their
+ * sends and numbers; one it lacks or changed refuses the module while sends
+ * are left on it (CONFLICT), and otherwise keeps its number for later. */
+PURE_COLD static PureResult remap_outputs(PureApp *app, const PureModule *old, const PureModule *module, bool apply) {
+ int map[PURE_MODULE_INPUTS];
+ for (uint32_t i = 0; i < old->inputs; ++i) {
+ map[i] = -1;
+ if (old->input[i].kind != PURE_KIND_OUTPUT) continue;
+ int to = pure_module_input(module, old->input_names[i]);
+ bool same = to >= 0 && module->input[to].kind == PURE_KIND_OUTPUT &&
+ module->input[to].field_count == old->input[i].field_count;
+ for (uint32_t f = 0; same && f < old->input[i].field_count; ++f)
+ same = module->field[module->input[to].field_start + f].type == old->field[old->input[i].field_start + f].type;
+ map[i] = same ? to : -1;
+ for (size_t s = 0; !same && s < app->sent_count; ++s)
+ if (app->sent[s].output == i) return PURE_CONFLICT;
+ }
+ if (!apply) return PURE_OK;
+ uint64_t last[PURE_MODULE_INPUTS] = {0}, saved[PURE_MODULE_INPUTS] = {0};
+ for (uint32_t i = 0; i < old->inputs; ++i) {
+ if (old->input[i].kind != PURE_KIND_OUTPUT || !app->last_sent[i]) continue;
+ if (map[i] >= 0) {
+ last[map[i]] = app->last_sent[i];
+ saved[map[i]] = app->saved_sent[i];
+ continue;
+ }
+ struct PureCarried *grown = realloc(app->carried, (app->carried_count + 1) * sizeof(*grown));
+ if (!grown) continue; /* its numbers would start again: kept only while memory allows */
+ app->carried = grown;
+ memcpy(grown[app->carried_count].name, old->input_names[i], PURE_MODULE_NAME);
+ grown[app->carried_count++].last = app->last_sent[i];
+ app->links_changed = true;
+ }
+ for (unsigned c = 0; c < app->carried_count;) {
+ int to = pure_module_input(module, app->carried[c].name);
+ if (to < 0 || module->input[to].kind != PURE_KIND_OUTPUT || last[to]) { ++c; continue; }
+ last[to] = saved[to] = app->carried[c].last;
+ app->carried[c] = app->carried[--app->carried_count];
+ app->links_changed = true;
+ }
+ for (size_t s = 0; s < app->sent_count; ++s) app->sent[s].output = (uint32_t)map[app->sent[s].output];
+ memcpy(app->last_sent, last, sizeof(last));
+ memcpy(app->saved_sent, saved, sizeof(saved));
+ return PURE_OK;
+}
+
+PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) {
+ const PureModule *old = app->module;
+ bool stored = pure_save_enabled(app);
+ double values[PURE_MODULE_STATES] = {0};
+ PureDraft *texts[PURE_MODULE_STATES] = {0}, *list_drafts[PURE_MODULE_STATES] = {0};
+ PureList *lists[PURE_MODULE_STATES] = {0};
+ PureVersion saved[PURE_MODULE_STATES] = {{0, 0}}, before[PURE_MODULE_STATES] = {{0, 0}};
+ int kept[PURE_MODULE_STATES];
+ bool rewrite[PURE_MODULE_STATES] = {false};
+ PureResult result = PURE_OK;
+ for (uint32_t i = 0; i < module->states; ++i) {
+ int previous = old ? pure_module_state(old, module->state_names[i]) : -1;
+ kept[i] = previous >= 0 && old->state_type[previous] == module->state_type[i] ? previous : -1;
+ if (kept[i] >= 0) saved[i] = app->saved[kept[i]];
+ if (module->state_type[i] == PURE_STATE_TEXT) {
+ if (kept[i] >= 0) texts[i] = app->texts[kept[i]];
+ else if (result == PURE_OK)
+ result = stored ? pure_save_open_text(app, module, i, &texts[i], &saved[i]) :
+ 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]);
+ list_drafts[i] = app->list_drafts[kept[i]];
+ rewrite[i] = result == PURE_OK && list_drafts[i] && lists[i] != app->lists[kept[i]];
+ } else if (stored) {
+ result = pure_save_open_list(app, module, i, &lists[i], &list_drafts[i], &saved[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 keyed list keeps its items in key order: one that was not keyed, or
+ * keyed by another field, is put in order and its indices follow their
+ * items. Two items with one key refuse the module: nothing is dropped. */
+ size_t *moved[PURE_MODULE_STATES] = {0};
+ for (uint32_t i = 0; i < module->states && result == PURE_OK; ++i) {
+ if (module->state_type[i] != PURE_STATE_LIST || !module->state_key[i]) continue;
+ size_t count = pure_list_count(lists[i]);
+ PureList *sorted = NULL;
+ if (count && !(moved[i] = malloc(count * sizeof(**moved)))) result = PURE_ALLOCATION;
+ else result = pure_list_sorted(lists[i], module->state_key[i] - 1u, &sorted, moved[i]);
+ if (!sorted) {
+ free(moved[i]);
+ moved[i] = NULL;
+ continue;
+ }
+ if (kept[i] < 0 || lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]);
+ lists[i] = sorted;
+ rewrite[i] = list_drafts[i] != NULL;
+ }
+ /* 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 && 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 && stored) result = pure_save_open_values(app, module, values, kept, texts, lists);
+ /* Outputs keep their sends and numbers by name. */
+ if (result == PURE_OK) result = old ? remap_outputs(app, old, module, false) : stored ? pure_save_open_links(app, module) : PURE_OK;
+ for (uint32_t i = 0; i < module->states; ++i) {
+ uint32_t list = module->state_ref[i];
+ if (module->state_type[i] == PURE_STATE_INDEX && moved[list] && values[i] < (double)pure_list_count(lists[list]))
+ values[i] = (double)moved[list][(size_t)values[i]];
+ }
+ for (uint32_t i = 0; i < module->states; ++i) free(moved[i]);
+ /* Store the lists whose record or order changed last, so that a failure
+ * can put every draft back as it was. */
+ for (uint32_t i = 0; i < module->states; ++i)
+ if (rewrite[i]) {
+ before[i] = pure_draft_version(list_drafts[i]);
+ pure_draft_hold(list_drafts[i], true);
+ }
+ for (uint32_t i = 0; i < module->states && result == PURE_OK; ++i)
+ if (rewrite[i]) result = pure_save_rewrite_list(module, i, lists[i], list_drafts[i]);
+ for (uint32_t i = 0; i < module->states; ++i)
+ if (rewrite[i]) {
+ if (result != PURE_OK) pure_draft_restore(list_drafts[i], before[i]);
+ pure_draft_hold(list_drafts[i], false);
+ }
+ if (result != PURE_OK) {
+ for (uint32_t i = 0; i < module->states; ++i) {
+ if (kept[i] >= 0) {
+ if (lists[i] && lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]);
+ continue;
+ }
+ pure_draft_close(texts[i]);
+ pure_list_free(lists[i]);
+ pure_draft_close(list_drafts[i]);
+ }
+ return result;
+ }
+ for (uint32_t i = 0; old && i < old->states; ++i) {
+ bool reused = false, draft_reused = false;
+ 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]);
+ draft_reused = draft_reused || (list_drafts[k] && list_drafts[k] == app->list_drafts[i]);
+ }
+ if (!reused) {
+ pure_draft_close(app->texts[i]);
+ pure_list_free(app->lists[i]);
+ }
+ if (!draft_reused) pure_draft_close(app->list_drafts[i]);
+ }
+ memcpy(app->values, values, sizeof(values));
+ memcpy(app->texts, texts, sizeof(texts));
+ memcpy(app->lists, lists, sizeof(lists));
+ memcpy(app->list_drafts, list_drafts, sizeof(list_drafts));
+ memcpy(app->saved, saved, sizeof(saved));
+ if (old) remap_outputs(app, old, module, true);
+ app->module = module;
+ ++app->changes;
+ return PURE_OK;
+}
+
+PURE_COLD PureResult pure_app_start(PureApp *app, const PureModule *module, PureData data) {
+ *app = (PureApp){.data = data, .fault_pc = UINT32_MAX};
+ PureResult result = open_module(app, module);
+ if (result == PURE_OK) result = pure_app_save(app);
+ if (result != PURE_OK) pure_app_stop(app);
+ return result;
+}
+
+PURE_COLD PureResult pure_app_replace(PureApp *app, const PureModule *module) {
+ pure_app_finish(app);
+ if (app->open || app->pending_count) {
+ PureResult saved = pure_app_save(app); /* the burst belongs to the module that made it */
+ if (saved != PURE_OK) return saved;
+ }
+ return open_module(app, module);
+}
diff --git a/core/module.h b/core/module.h
new file mode 100644
index 0000000..f62326b
--- /dev/null
+++ b/core/module.h
@@ -0,0 +1,343 @@
+#ifndef PURE_MODULE_H
+#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 v5 (docs/design/008-module-format.md). A toolchain such
+ * as aup produces it; Pure verifies it completely before anything executes:
+ * operand types (including which text a cursor and which list an index
+ * belongs to), stack heights and the stack at every join, forward-only
+ * branches, names, records, tables, constants and effects. */
+enum {
+ PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 64,
+ PURE_MODULE_HANDLERS = 64, PURE_MODULE_CODE = 65535, PURE_MODULE_STACK = 64,
+ PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 40,
+ PURE_MODULE_FIELDS = 64, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 4096,
+ PURE_MODULE_CONSTANT_BYTES = 1 << 20, PURE_MODULE_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64,
+ PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8, PURE_APP_PENDING = 256
+};
+
+typedef enum {
+ PURE_OP_CONST, PURE_OP_LOAD, PURE_OP_DT, PURE_OP_ADD, PURE_OP_SUB, PURE_OP_MUL,
+ PURE_OP_DIV, PURE_OP_NEG, PURE_OP_LT, PURE_OP_LE, PURE_OP_GT, PURE_OP_GE,
+ PURE_OP_EQ, PURE_OP_NE, PURE_OP_AND, PURE_OP_OR, PURE_OP_NOT, PURE_OP_MIN,
+ PURE_OP_MAX, PURE_OP_CLAMP, PURE_OP_STORE, PURE_OP_JZ, PURE_OP_JMP,
+ PURE_OP_FIELD, PURE_OP_DOC, PURE_OP_DOC_SIZE, PURE_OP_LEN, PURE_OP_REPLACE,
+ PURE_OP_REPLY, PURE_OP_REQUIRE,
+ PURE_OP_TEXT_CONST, PURE_OP_TEXT_LOAD, PURE_OP_TEXT_INPUT, PURE_OP_NUM_TEXT,
+ PURE_OP_CONCAT, PURE_OP_CHARS, PURE_OP_LINES, PURE_OP_TEXT_BYTES,
+ 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_ROW,
+ PURE_OP_PICK, PURE_OP_SLIDE, PURE_OP_FOLD, PURE_OP_TEXT_COMPARE, PURE_OP_LIST_FIND, PURE_OP_LIST_HAS,
+ PURE_OP_NOW, PURE_OP_FLOOR, PURE_OP_MOD, PURE_OP_SEND,
+ PURE_OP_COUNT
+} PureOpcode;
+/* FOLD's argument: the kind (bits 0-7), the list state (8-15) and the length
+ * of the block that follows (16-31), run once per item as a column of that
+ * list. count counts the items whose block leaves a non-zero number; sum,
+ * max and min take a value and an include flag from their block; first gives
+ * the index of the first item whose block leaves a non-zero number. */
+typedef enum { PURE_FOLD_COUNT, PURE_FOLD_SUM, PURE_FOLD_MAX, PURE_FOLD_MIN, PURE_FOLD_FIRST } PureFold;
+/* TEXT_COMPARE's argument: the relation, in the order of the number
+ * comparisons (LT LE GT GE EQ NE); texts order by their bytes. */
+typedef enum { PURE_LESS, PURE_LESS_EQUAL, PURE_GREATER, PURE_GREATER_EQUAL, PURE_EQUAL, PURE_NOT_EQUAL } PureRelation;
+
+/* A timer ticks every `period` milliseconds while a host runs the app. An
+ * output is a message the app sends (SEND, in a handler; it has fields as a
+ * message has and no handler): what receives it is the host's wiring. */
+typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3, PURE_KIND_TIMER = 4,
+ PURE_KIND_OUTPUT = 5 } PureInputKind;
+enum { PURE_TIMER_LONGEST = 86400000, PURE_SOURCE = 64 };
+/* A message field: a u32le or a binary64 (fixed, at its offset in the
+ * payload's prefix), or bytes or text of any length. The prefix also holds
+ * each variable field's u32le length but the last one's (its offset is
+ * PURE_FIELD_REST), which takes what follows the others; the variable
+ * fields' bytes follow the prefix in declaration order. A text is UTF-8 and
+ * a number finite, or the message fails. */
+typedef enum { PURE_FIELD_U32 = 1, PURE_FIELD_BYTES = 2, PURE_FIELD_NUMBER = 3, PURE_FIELD_TEXT = 4 } PureFieldType;
+enum { PURE_FIELD_REST = UINT32_MAX };
+typedef enum {
+ PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4
+} PureStateType;
+/* A cursor that stays before text inserted at it (a mark); other cursors go
+ * after it (a caret). */
+enum { PURE_CURSOR_STAYS = 1 };
+/* A table view has one row per item of a list, or per item its filter
+ * keeps; its columns are number or text views evaluated for one row at a
+ * time. */
+typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2 } PureViewType;
+typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
+typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge;
+/* EXISTS: an item with that key is already in the keyed list (the event
+ * reports PURE_CONFLICT). */
+typedef enum {
+ PURE_FAULT_NONE, PURE_FAULT_DIVIDE, PURE_FAULT_NON_FINITE, PURE_FAULT_CLAMP,
+ PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND, PURE_FAULT_ENCODING, PURE_FAULT_EXISTS
+} PureFault;
+
+typedef struct { double value; uint32_t arg; uint8_t op; } PureOp;
+typedef struct { uint32_t start, length; } PureBlock;
+typedef struct { uint32_t event; PureBlock code; } PureHandler;
+typedef struct { char name[PURE_MODULE_NAME]; uint8_t type; uint32_t offset; } PureField;
+typedef struct {
+ uint8_t kind;
+ bool keyed;
+ uint16_t field_start, field_count;
+ uint32_t prefix; /* fixed payload bytes before the variable fields */
+ uint32_t period; /* a timer's, in milliseconds */
+} 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, 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];
+ char view_names[PURE_MODULE_VIEWS][PURE_MODULE_NAME];
+ 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: constant; cursor: text; list: record; index: list */
+ uint8_t state_flags[PURE_MODULE_STATES]; /* cursor: PURE_CURSOR_STAYS */
+ /* A keyed list: its key field + 1 (0: none). Its items stay in key order
+ * (numbers by value, texts by bytes), each key once, and keys do not change. */
+ uint8_t state_key[PURE_MODULE_STATES];
+ PureInputShape input[PURE_MODULE_INPUTS];
+ PureField field[PURE_MODULE_FIELDS];
+ double initial[PURE_MODULE_STATES];
+ uint8_t view_type[PURE_MODULE_VIEWS];
+ uint16_t view_table[PURE_MODULE_VIEWS]; /* a column's table view + 1, or 0 */
+ uint16_t view_list[PURE_MODULE_VIEWS]; /* the list of a table or column */
+ PureBlock view_code[PURE_MODULE_VIEWS];
+ PureHandler handler[PURE_MODULE_HANDLERS];
+ /* The code and the text constants, sized to the module in one block
+ * (storage) that load takes and pure_module_release gives back. */
+ PureOp *code;
+ uint32_t *constant_offset, *constant_length;
+ unsigned char *constant_data;
+ void *storage;
+ 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 */
+} PureModule;
+
+/* Decode and verify into out, which must be zeroed (a static one is) or hold
+ * a loaded module: what it held is given back first. On failure out holds
+ * nothing and error names the rule. */
+PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
+ char *error, size_t error_size);
+/* Give back a loaded module's code and constants (no app may still run it). */
+void pure_module_release(PureModule *);
+/* Read a module file whole and load it (NOT_FOUND when it cannot be read). */
+PureResult pure_module_read(const char *path, PureModule *out, char *error, size_t error_size);
+/* Index of a name, or -1. Views are those outside tables. */
+int pure_module_state(const PureModule *, const char *name);
+int pure_module_input(const PureModule *, const char *name);
+int pure_module_view(const PureModule *, const char *name);
+int pure_module_column(const PureModule *, uint32_t table, const char *name);
+/* The index state a number view shows as its position (a view that is just
+ * `at.position`), or -1: what a host moves with pure_app_point. */
+int pure_module_position(const PureModule *, uint32_t view);
+/* The text state a text view shows as it is (a view that is just the
+ * state), or -1: a host may lend its stored extents instead of copying. */
+int pure_module_text_state(const PureModule *, uint32_t view);
+
+/* Keyed collections live in a data host (for example the tree store). One
+ * event opens at most one document, the message's key, at its current
+ * version; edits stay in that working version until the event commits it.
+ * A version's bytes field is the value's size. */
+typedef struct {
+ void *context;
+ PureResult (*open)(void *context, uint32_t collection, const char *key,
+ void **document, int64_t *revision);
+ PureVersion (*version)(void *context, void *document);
+ PureResult (*read)(void *context, void *document, PureVersion, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *visit_context);
+ PureResult (*splice)(void *context, void *document, size_t offset, size_t remove, PureSpan insert);
+ PureResult (*commit)(void *context, void *document, int64_t *revision);
+ void (*close)(void *context, void *document);
+ /* Optional: the host keeps the app's states. state opens a runtime-owned
+ * value by name as a draft (a missing one is empty at revision 0); save
+ * publishes the given documents and state drafts together, all or
+ * nothing, and names each document's new revision. */
+ PureResult (*state)(void *context, const char *name, PureDraft **out);
+ PureResult (*save)(void *context, void *const *documents, size_t document_count, PureDraft *const *states,
+ size_t count, int64_t *revisions);
+} PureData;
+
+/* A byte or text value inside one event or view: a range of the payload, a
+ * document or text version, a module constant, a copy or the event's arena. */
+typedef struct { uint8_t source; uint16_t owner; PureVersion version; uint64_t offset, length; } PureBytes;
+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; 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.
+ *
+ * Saving. When the data host keeps states the app starts from the stored
+ * ones. A save publishes, in one commit, the documents edited by messages and
+ * the states changed since the last save; if it fails, every event since the
+ * last save is undone. By default each key, text and message event saves
+ * before it returns (a frame waits for the next one). Deferred, events apply
+ * at once and a host saves a burst with one commit: an event is durable once
+ * a later save succeeds, and each key's document joins a burst once (a second
+ * message for it, or one past PURE_APP_PENDING keys, saves the burst first). */
+typedef struct {
+ const PureModule *module;
+ PureData data;
+ 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 */
+ PureDraft *list_drafts[PURE_MODULE_STATES]; /* stored lists, when kept */
+ PureDraft *stored_values; /* numbers, cursors and indices, when kept */
+ PureVersion saved[PURE_MODULE_STATES]; /* text and list versions at the last save */
+ /* Events since the last save: what a failed save returns to, and the
+ * documents messages opened (held until the save). */
+ bool deferred, open;
+ double durable_values[PURE_MODULE_STATES];
+ PureVersion durable_texts[PURE_MODULE_STATES], durable_lists[PURE_MODULE_STATES];
+ uint64_t durable_ticks, durable_events;
+ struct PurePending { void *document; char key[65]; bool edited; } *pending; /* grows to PURE_APP_PENDING */
+ unsigned pending_count, pending_capacity;
+ PureFault fault;
+ uint32_t fault_pc;
+ uint64_t ticks, events;
+ /* The time events see as now, in milliseconds since 1970 (UTC). The host
+ * sets it, so a script can replay any time; views never see it. */
+ double now;
+ uint64_t changes; /* counts every change of states, never back: what a filter's rows are fresh for */
+ struct PureRows { uint32_t table; uint64_t changes; size_t *items, count, capacity; } *rows; /* a filtered table's items */
+ unsigned row_tables;
+ /* Current message or text event, kept until pure_app_finish. */
+ void *document;
+ PureSpan payload;
+ bool has_reply;
+ PureBytes reply;
+ unsigned char *copied;
+ int64_t revision;
+ PureArena arena, view_arena;
+ /* Links (runtime-owned, saved with the states): messages sent on outputs
+ * and not trimmed, in the order sent (the first sent_durable are saved;
+ * a trimmed one goes at the next save), each output's last number (and
+ * as saved), those of outputs a module dropped, and the last number
+ * delivered from each source (and as saved). */
+ struct PureSent { uint32_t output; uint64_t seq; unsigned char *payload; size_t size; bool trimmed; } *sent;
+ size_t sent_count, sent_capacity, sent_durable;
+ uint64_t last_sent[PURE_MODULE_INPUTS], saved_sent[PURE_MODULE_INPUTS];
+ struct PureCarried { char name[PURE_MODULE_NAME]; uint64_t last; } *carried;
+ unsigned carried_count;
+ struct PureHeard { char source[PURE_SOURCE + 1]; uint64_t seq, saved; } *heard;
+ unsigned heard_count, heard_capacity;
+ PureDraft *stored_links;
+ bool links_changed; /* since the last save */
+ bool duplicate; /* the last delivery was heard before and not applied */
+ struct { const char *source; uint64_t seq; bool pending; } delivering;
+ struct PureSnapshot *snapshot; /* the newest snapshot made, or NULL */
+ PureRetired retired; /* list items let go since it was made */
+ bool reading; /* a reader: its states are a snapshot's */
+} PureApp;
+
+PureResult pure_app_start(PureApp *, const PureModule *, PureData data);
+/* Publish every change since the last save in one commit, or undo them all. */
+PureResult pure_app_save(PureApp *);
+/* Defer saving to pure_app_save (false: save after each event). Needs a data
+ * host that saves documents together (PureData.save). */
+bool pure_app_defer(PureApp *, bool deferred);
+/* Leaves unsaved frame changes unsaved: call pure_app_save first to keep them. */
+void pure_app_stop(PureApp *);
+PureResult pure_app_frame(PureApp *);
+/* A timer input's tick: an event saved like a key's. */
+PureResult pure_app_tick(PureApp *, uint32_t input);
+PureResult pure_app_input(PureApp *, uint32_t input, PureEdge);
+/* Point an index state at the item at position (0 first), as a user pointing
+ * at a row: an event with no handler, saved like a key event. NOT_FOUND past
+ * the last item, INVALID for a state that is not an index. */
+PureResult pure_app_point(PureApp *, uint32_t state, size_t position);
+/* Deliver committed text (for example from an input method). Not UTF-8: invalid. */
+PureResult pure_app_text(PureApp *, uint32_t input, PureSpan text);
+/* Deliver a message. base is the revision the sender edited, or -1 for none;
+ * a handler that edits the document needs one. payload stays alive until
+ * pure_app_finish. On return app->revision is the document's revision
+ * (deferred: the revision the next save gives it). */
+PureResult pure_app_message(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload);
+/* The same, with the variable fields where the host found them: field f
+ * starts at placed[2f] in payload and has placed[2f + 1] bytes (entries of
+ * fixed fields are not read, nor are the variable fields' lengths in the
+ * prefix). A host that parsed a request in place passes the fields' bytes
+ * where they are. */
+PureResult pure_app_message_fields(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed);
+/* Visit the reply of the last message in order, without copying, until the
+ * next event (deferred: copy it before a later save too). */
+PureResult pure_app_reply(const PureApp *, bool (*visit)(void *, PureSpan), void *context);
+void pure_app_finish(PureApp *);
+/* A number view; false for a text view or a failed evaluation. */
+bool pure_app_view(PureApp *, uint32_t view, double *out);
+/* A text view, lent in order until the next view evaluation. */
+PureResult pure_app_view_text(PureApp *, uint32_t view, bool (*visit)(void *, PureSpan), void *context);
+/* The rows of a table view (its list's items, or those its filter keeps),
+ * or 0 for another view. A host evaluates only the cells it shows. */
+size_t pure_app_rows(PureApp *, uint32_t table);
+/* The item a row of a table shows, and the row that shows an item (false:
+ * its filter leaves the item out). Without a filter both are the identity. */
+bool pure_app_row_item(PureApp *, uint32_t table, size_t row, size_t *item);
+bool pure_app_item_row(PureApp *, uint32_t table, size_t item, size_t *row);
+bool pure_app_cell(PureApp *, uint32_t column, size_t row, double *out);
+PureResult pure_app_cell_text(PureApp *, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context);
+/* Switch to another verified module. States keep their value by name (text
+ * states keep their content, cursors their position in that text); new states
+ * start at their initial value. The module must outlive the app. */
+PureResult pure_app_replace(PureApp *, const PureModule *);
+
+/* Connections between apps (docs/research/078-app-connections.md). A host
+ * sees only saved sends, delivers each at least once, in order per output,
+ * and trims what its receiver has saved; a receiver applies each once. */
+typedef bool (*PureSentVisit)(void *context, uint32_t output, uint64_t seq, PureSpan payload);
+void pure_app_outbox(const PureApp *, PureSentVisit visit, void *context);
+/* The same, newest first (a host looking only for what is new stops early). */
+void pure_app_outbox_newest(const PureApp *, PureSentVisit visit, void *context);
+/* Forget output's saved sends numbered up to through (saved with the next save). */
+void pure_app_trim(PureApp *, uint32_t output, uint64_t through);
+/* Deliver a message event from source (a sender's name, 1 to 64 printable
+ * ASCII bytes) numbered seq (from 1, growing per source). A number heard
+ * before is not applied again: it succeeds with app->duplicate set. The
+ * number counts as heard with the event's changes, and also when the app
+ * refuses the message (a fault: it was delivered, and refused); not when a
+ * save fails. placed is as for pure_app_message_fields, or NULL. */
+PureResult pure_app_deliver(PureApp *, uint32_t input, const char *source, uint64_t seq, PureSpan payload,
+ const uint32_t *placed);
+
+/* Readers on other threads. Right after a save an app can make a snapshot of
+ * its saved states, for evaluating views anywhere while it keeps applying
+ * events: numbers are copied; text states share their stored versions and
+ * lists their items (a list unchanged since the last snapshot shares that
+ * snapshot's copy), so the cost does not grow with the text. NULL when a
+ * change waits for a save, when a text state is not all stored (for example
+ * without a data host that keeps states), or when memory runs out. The caller
+ * gets a reference; the app keeps its own until the next snapshot. */
+typedef struct PureSnapshot PureSnapshot;
+PureSnapshot *pure_app_snapshot(PureApp *);
+void pure_snapshot_hold(PureSnapshot *);
+/* On any thread; the last reference frees it. */
+void pure_snapshot_release(PureSnapshot *);
+/* Show a snapshot's states in reader, a zeroed app or a reader before: its
+ * views read them while the snapshot is held, with the reader's own arena and
+ * row cache (one thread at a time). It applies no events. pure_app_stop frees
+ * the reader and leaves the snapshot alone. */
+void pure_app_read(PureApp *reader, const PureSnapshot *);
+#endif
diff --git a/core/result.h b/core/result.h
new file mode 100644
index 0000000..8abb0f2
--- /dev/null
+++ b/core/result.h
@@ -0,0 +1,8 @@
+#ifndef PURE_RESULT_H
+#define PURE_RESULT_H
+typedef enum {
+ PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET,
+ PURE_LIMIT, PURE_STORE_ERROR, PURE_UNCERTAIN,
+ PURE_INVALID, PURE_DENIED, PURE_CLOSED, PURE_CANCELLED, PURE_ALLOCATION
+} PureResult;
+#endif
diff --git a/core/runtime.c b/core/runtime.c
new file mode 100644
index 0000000..4a857c7
--- /dev/null
+++ b/core/runtime.c
@@ -0,0 +1,394 @@
+#include "runtime.h"
+#include <string.h>
+
+#define NONE SIZE_MAX
+enum { FREE, RECEIVING, READY, RUNNING, DONE };
+#ifdef PURE_RUNTIME_METRICS
+#define METRIC(...) do { __VA_ARGS__; } while (0)
+#else
+#define METRIC(...) ((void)0)
+#endif
+
+static PureWork *lookup(PureTicket ticket, bool require_held) {
+ PureRuntime *r = ticket.runtime;
+ if (!r || ticket.slot >= r->capacity || !ticket.generation) return NULL;
+ PureWork *w = &r->work[ticket.slot];
+ if (w->state == FREE || w->generation != ticket.generation ||
+ (require_held && !w->held)) return NULL;
+ return w;
+}
+
+static bool valid_key(const char *key) {
+ if (!key || !*key) return false;
+ size_t n = 0;
+ for (; n <= 64 && key[n]; ++n) {
+ unsigned char c = (unsigned char)key[n];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '-' || c == '_')) return false;
+ }
+ return n <= 64;
+}
+
+/* WRITE results are loans from the admitted input, not a second allocation.
+ * Integer offsets avoid relational comparisons between unrelated C pointers. */
+static bool borrowed_reply(PureSpan input, PureSpan reply) {
+ if (!reply.size) return true;
+ if (!input.data || !reply.data) return false;
+ uintptr_t base = (uintptr_t)input.data, start = (uintptr_t)reply.data;
+ return start >= base && start - base <= input.size &&
+ reply.size <= input.size - (start - base);
+}
+
+static void dispose(PureRuntime *r, PureResource *resource) {
+ PureResource old = *resource;
+ *resource = (PureResource){0};
+ if (old.release) {
+ bool running = r->running;
+ r->running = true;
+ old.release(old.allocation, old.allocation_size);
+ r->running = running;
+ }
+}
+
+static void payload_release(PureRuntime *r, PureWork *w);
+
+/* Iteratively retire a chain of forwarded values. An arbitrarily long chain
+ * must not turn completion into recursive stack growth. */
+static void retire(PureRuntime *r, PureWork *w) {
+ while (w && !w->held && !w->pins) {
+ PureTicket parent = w->parent;
+#ifdef PURE_RUNTIME_METRICS
+ bool released_payload = w->charged || w->resource.release || parent.runtime;
+#endif
+ dispose(r, &w->resource);
+ r->retained -= w->charged;
+ METRIC(if (released_payload) ++r->releases);
+ size_t slot = (size_t)(w - r->work);
+ *w = (PureWork){.next = r->free_head};
+ r->free_head = slot;
+ --r->active;
+ if (!parent.runtime) break;
+ r = parent.runtime;
+ w = lookup(parent, false);
+ /* Pins can only be created and removed by the owner-thread core. */
+ if (!w || !w->pins) break;
+ --w->pins;
+ }
+}
+
+static void payload_release(PureRuntime *r, PureWork *w) {
+ PureTicket parent = w->parent;
+#ifdef PURE_RUNTIME_METRICS
+ bool released_payload = w->charged || w->resource.release || parent.runtime;
+#endif
+ dispose(r, &w->resource);
+ r->retained -= w->charged;
+ METRIC(if (released_payload) ++r->releases);
+ w->charged = 0;
+ w->parent = (PureTicket){0};
+ if (parent.runtime) {
+ PureWork *source = lookup(parent, false);
+ if (source && source->pins) { --source->pins; retire(parent.runtime, source); }
+ }
+}
+
+bool pure_runtime_init(PureRuntime *r, PureWork *work, size_t capacity,
+ const void *definition, PureHost host, size_t budget) {
+ if (!r || ((work == NULL) != (capacity == 0))) return false;
+ *r = (PureRuntime){.definition = definition, .host = host, .free_head = NONE,
+ .ready_head = NONE, .ready_tail = NONE,
+ .budget = budget, .accepting = true};
+ return pure_runtime_bind(r, work, capacity);
+}
+
+bool pure_runtime_bind(PureRuntime *r, PureWork *work, size_t capacity) {
+ if (!r || r->running || r->active || r->retained ||
+ r->ready_head != NONE || r->ready_tail != NONE ||
+ ((work == NULL) != (capacity == 0)))
+ return false;
+ r->work = work;
+ r->capacity = capacity;
+ r->free_head = capacity ? 0 : NONE;
+ for (size_t i = 0; i < capacity; ++i)
+ work[i] = (PureWork){.next = i + 1 < capacity ? i + 1 : NONE};
+ return true;
+}
+
+bool pure_runtime_restart(PureRuntime *r, const void *definition, PureHost host, size_t budget) {
+ if (!r || r->running || r->active) return false;
+ r->definition = definition;
+ r->host = host;
+ r->budget = budget;
+ r->accepting = true;
+ return true;
+}
+
+void pure_runtime_stop(PureRuntime *r) { if (r && !r->running) r->accepting = false; }
+
+static PureResult admission(PureRuntime *r, PureInput input, const char *key,
+ int64_t expected, size_t bytes) {
+ if (!r || r->running || !valid_key(key) ||
+ (input != PURE_INPUT_READ && input != PURE_INPUT_WRITE) ||
+ (input == PURE_INPUT_READ ? bytes != 0 : expected < 0)) return PURE_INVALID;
+ if (!r->accepting) return PURE_CLOSED;
+ bool missing_effect = input == PURE_INPUT_READ ? !r->host.read : !r->host.write;
+#ifdef PURE_ASYNC_EFFECTS
+ if (missing_effect && !r->async_effects) return PURE_DENIED;
+#else
+ if (missing_effect) return PURE_DENIED;
+#endif
+ if (r->free_head == NONE) return PURE_BUSY;
+ if (r->serial == UINT64_MAX) return PURE_LIMIT;
+ if (bytes > r->budget - r->retained) { METRIC(++r->budget_rejections); return PURE_BUDGET; }
+ return PURE_OK;
+}
+
+static PureTicket occupy(PureRuntime *r, PureInput input, const char *key,
+ int64_t expected, size_t bytes, PureResource resource) {
+ size_t slot = r->free_head;
+ PureWork *w = &r->work[slot];
+ r->free_head = w->next;
+ *w = (PureWork){.generation = ++r->serial, .next = NONE, .charged = bytes,
+ .state = RECEIVING, .held = true, .input = input, .expected = expected,
+ .resource = resource};
+ memcpy(w->key, key, strlen(key) + 1);
+ ++r->active;
+ METRIC(++r->accepted);
+ r->retained += bytes;
+ METRIC(if (r->active > r->peak_active) r->peak_active = r->active);
+ METRIC(if (r->retained > r->peak_retained) r->peak_retained = r->retained);
+ return (PureTicket){r, slot, w->generation};
+}
+
+bool pure_runtime_ready(PureTicket ticket) {
+ PureWork *w = lookup(ticket, true);
+ if (!w || ticket.runtime->running || w->state != RECEIVING) return false;
+ PureRuntime *r = ticket.runtime;
+ w->state = READY;
+ if (r->ready_tail == NONE) r->ready_head = ticket.slot;
+ else r->work[r->ready_tail].next = ticket.slot;
+ r->ready_tail = ticket.slot;
+ return true;
+}
+
+PureResult pure_runtime_accept(PureRuntime *r, PureInput input, const char *key,
+ int64_t expected, size_t size, PureTicket *out) {
+ if (!out) return PURE_INVALID;
+ PureResult status = admission(r, input, key, expected, size);
+ if (status != PURE_OK) return status;
+ PureResource resource = {0};
+ if (size) {
+ if (!r->host.allocate) return PURE_ALLOCATION;
+ r->running = true;
+ bool ok = r->host.allocate(r->host.context, size, &resource);
+ r->running = false;
+ if (!ok || !resource.body.data || resource.body.size != size || !resource.release) {
+ dispose(r, &resource);
+ return PURE_ALLOCATION;
+ }
+ }
+ *out = occupy(r, input, key, expected, size, resource);
+ if (input == PURE_INPUT_READ) pure_runtime_ready(*out);
+ return PURE_OK;
+}
+
+unsigned char *pure_runtime_ingress(PureTicket ticket, size_t *size) {
+ PureWork *w = lookup(ticket, true);
+ if (size) *size = 0;
+ if (!w || ticket.runtime->running || w->state != RECEIVING ||
+ w->input != PURE_INPUT_WRITE || w->parent.runtime) return NULL;
+ if (size) *size = w->resource.body.size;
+ return (unsigned char *)w->resource.body.data;
+}
+
+bool pure_runtime_step(PureRuntime *r) {
+ if (!r || r->running || r->ready_head == NONE) return false;
+#ifdef PURE_ASYNC_EFFECTS
+ PureWork *head = &r->work[r->ready_head];
+ if (head->input == PURE_INPUT_READ ? !r->host.read : !r->host.write) return false;
+#endif
+ size_t slot = r->ready_head;
+ PureWork *w = &r->work[slot];
+ r->ready_head = w->next;
+ if (r->ready_head == NONE) r->ready_tail = NONE;
+ w->next = NONE;
+ w->state = RUNNING;
+ r->running = true;
+ PureOutcome outcome = {0};
+ if (w->input == PURE_INPUT_WRITE) {
+ PureSpan reply = {0};
+ outcome.status = r->host.write(r->host.context, r->definition, w->key,
+ w->expected, w->resource.body, &reply,
+ &outcome.revision);
+ if (outcome.status == PURE_OK) {
+ if (borrowed_reply(w->resource.body, reply)) outcome.body = reply;
+ else outcome.status = PURE_STORE_ERROR;
+ }
+ METRIC(if (outcome.status == PURE_OK) ++r->commits;
+ else if (outcome.status == PURE_CONFLICT) ++r->conflicts);
+ } else {
+ outcome.status = r->host.read(r->host.context, r->definition, w->key,
+ r->budget - r->retained, &w->resource, &outcome.revision);
+ if (outcome.status == PURE_OK) {
+ if (w->resource.body.size > r->budget - r->retained) outcome.status = PURE_BUDGET;
+ else if (w->resource.body.size && !w->resource.release)
+ outcome.status = PURE_STORE_ERROR;
+ else {
+ w->charged = w->resource.body.size;
+ r->retained += w->charged;
+ METRIC(if (r->retained > r->peak_retained) r->peak_retained = r->retained);
+ outcome.body = w->resource.body;
+ METRIC(++r->gets);
+ }
+ }
+ METRIC(if (outcome.status == PURE_BUDGET) ++r->budget_rejections);
+ }
+ if (outcome.status != PURE_OK || !outcome.body.size) {
+ outcome.body = (PureSpan){0};
+ payload_release(r, w);
+ }
+ METRIC(r->borrowed_output_bytes += outcome.body.size);
+ w->outcome = outcome;
+ w->state = DONE;
+ r->running = false;
+ return true;
+}
+
+#ifdef PURE_ASYNC_EFFECTS
+bool pure_runtime_enable_async(PureRuntime *r) {
+ if (!r || r->running || r->active) return false;
+ r->async_effects = true;
+ return true;
+}
+
+PureTake pure_runtime_take(PureRuntime *r, PureEffect *out) {
+ if (!r || !out || !r->async_effects || r->running || r->ready_head == NONE)
+ return PURE_TAKE_EMPTY;
+ size_t slot = r->ready_head;
+ PureWork *w = &r->work[slot];
+ r->ready_head = w->next;
+ if (r->ready_head == NONE) r->ready_tail = NONE;
+ w->next = NONE;
+ w->state = RUNNING;
+ *out = (PureEffect){.ticket = {r, slot, w->generation}, .input = w->input,
+ .key = w->key, .expected = w->expected,
+ .value = w->resource.body,
+ .available = r->budget - r->retained};
+ return PURE_TAKE_EFFECT;
+}
+
+bool pure_runtime_complete(PureTicket ticket, PureResult status,
+ PureResource *read_resource, PureSpan write_reply,
+ int64_t revision) {
+ PureWork *w = lookup(ticket, false);
+ PureRuntime *r = ticket.runtime;
+ if (!w || r->running || w->state != RUNNING ||
+ (w->input == PURE_INPUT_WRITE && read_resource) ||
+ (w->input == PURE_INPUT_READ && write_reply.size) ||
+ (w->input == PURE_INPUT_WRITE && status == PURE_OK &&
+ !borrowed_reply(w->resource.body, write_reply)) ||
+ (read_resource && !read_resource->release &&
+ (read_resource->body.size || read_resource->allocation))) return false;
+ r->running = true;
+ PureOutcome outcome = {.status = status, .revision = revision};
+ if (w->input == PURE_INPUT_READ) {
+ PureResource acquired = {0};
+ if (read_resource) {
+ acquired = *read_resource;
+ *read_resource = (PureResource){0};
+ }
+ if (outcome.status == PURE_OK) {
+ if (acquired.body.size > r->budget - r->retained)
+ outcome.status = PURE_BUDGET;
+ else if (acquired.body.size && !acquired.release)
+ outcome.status = PURE_STORE_ERROR;
+ else {
+ w->resource = acquired;
+ acquired = (PureResource){0};
+ w->charged = w->resource.body.size;
+ r->retained += w->charged;
+ METRIC(if (r->retained > r->peak_retained) r->peak_retained = r->retained);
+ outcome.body = w->resource.body;
+ METRIC(++r->gets);
+ }
+ }
+ dispose(r, &acquired);
+ METRIC(if (outcome.status == PURE_BUDGET) ++r->budget_rejections);
+ } else {
+ if (status == PURE_OK) outcome.body = write_reply;
+ METRIC(if (outcome.status == PURE_OK) ++r->commits;
+ else if (outcome.status == PURE_CONFLICT) ++r->conflicts);
+ }
+ if (outcome.status != PURE_OK || !outcome.body.size) {
+ outcome.body = (PureSpan){0};
+ payload_release(r, w);
+ }
+ METRIC(r->borrowed_output_bytes += outcome.body.size);
+ w->outcome = outcome;
+ w->state = DONE;
+ r->running = false;
+ if (!w->held) retire(r, w);
+ return true;
+}
+#endif
+
+bool pure_runtime_result(PureTicket ticket, PureOutcome *out) {
+ PureWork *w = lookup(ticket, true);
+ if (!out || !w || ticket.runtime->running || w->state != DONE) return false;
+ *out = w->outcome;
+ return true;
+}
+
+PureResult pure_runtime_forward(PureTicket source, PureRuntime *r,
+ const char *key, int64_t expected, PureTicket *out) {
+ PureWork *parent = lookup(source, true);
+ if (!out || !parent || source.runtime->running || parent->state != DONE ||
+ parent->outcome.status != PURE_OK || (parent->outcome.body.size && !parent->outcome.body.data))
+ return PURE_INVALID;
+ PureResult status = admission(r, PURE_INPUT_WRITE, key, expected, parent->outcome.body.size);
+ if (status != PURE_OK) return status;
+ if (parent->pins == SIZE_MAX) return PURE_LIMIT;
+ PureTicket ticket = occupy(r, PURE_INPUT_WRITE, key, expected, parent->outcome.body.size,
+ (PureResource){.body = parent->outcome.body});
+ ++parent->pins;
+ r->work[ticket.slot].parent = source;
+ pure_runtime_ready(ticket);
+ *out = ticket;
+ return PURE_OK;
+}
+
+static void unlink_ready(PureRuntime *r, size_t slot) {
+ size_t previous = NONE;
+ for (size_t at = r->ready_head; at != NONE; at = r->work[at].next) {
+ if (at == slot) {
+ if (previous == NONE) r->ready_head = r->work[at].next;
+ else r->work[previous].next = r->work[at].next;
+ if (r->ready_tail == at) r->ready_tail = previous;
+ r->work[at].next = NONE;
+ return;
+ }
+ previous = at;
+ }
+}
+
+bool pure_runtime_cancel(PureTicket ticket) {
+ PureWork *w = lookup(ticket, true);
+ if (!w || ticket.runtime->running || (w->state != RECEIVING && w->state != READY)) return false;
+ if (w->state == READY) unlink_ready(ticket.runtime, ticket.slot);
+ payload_release(ticket.runtime, w);
+ w->outcome = (PureOutcome){.status = PURE_CANCELLED};
+ w->state = DONE;
+ return true;
+}
+
+bool pure_runtime_release(PureTicket ticket) {
+ PureWork *w = lookup(ticket, true);
+ if (!w || ticket.runtime->running) return false;
+ if (w->state == READY) unlink_ready(ticket.runtime, ticket.slot);
+ w->held = false;
+#ifdef PURE_ASYNC_EFFECTS
+ if (w->state == RUNNING) return true;
+#endif
+ retire(ticket.runtime, w);
+ return true;
+}
diff --git a/core/runtime.h b/core/runtime.h
new file mode 100644
index 0000000..70f5d08
--- /dev/null
+++ b/core/runtime.h
@@ -0,0 +1,150 @@
+#ifndef PURE_RUNTIME_H
+#define PURE_RUNTIME_H
+#include "flow.h"
+#include "result.h"
+#include <stdint.h>
+
+/* The optional metrics profile changes PureRuntime's layout. Give its init
+ * function a distinct link name so mixed-profile objects cannot link quietly. */
+#ifdef PURE_RUNTIME_METRICS
+#define pure_runtime_init pure_runtime_init_metrics
+#endif
+
+/* One owner thread controls admission, payload credits, completion and loans.
+ * An application definition is opaque to this core. Hosts interpret it and
+ * perform effects; no parser, storage, socket or OS type is linked here. */
+typedef struct PureRuntime PureRuntime;
+typedef struct PureWork PureWork;
+typedef struct { PureRuntime *runtime; size_t slot; uint64_t generation; } PureTicket;
+typedef enum { PURE_INPUT_READ, PURE_INPUT_WRITE } PureInput;
+
+/* The host owns this resource until it successfully returns it to the core.
+ * release must end all access and free/unmap allocation exactly once. It runs
+ * while that domain is busy; calls back into the same domain are rejected.
+ * A resource the host sends in pieces (such as extents of a mapping) has no
+ * contiguous body: body.data is NULL and body.size is still what it holds and
+ * is charged. Its outcome has no body to read and cannot be forwarded. */
+typedef struct {
+ PureSpan body;
+ void *allocation;
+ size_t allocation_size;
+ void (*release)(void *allocation, size_t size);
+} PureResource;
+typedef struct {
+ void *context;
+ bool (*allocate)(void *, size_t, PureResource *);
+ PureResult (*read)(void *, const void *definition, const char *,
+ size_t available, PureResource *, int64_t *revision);
+ /* A successful reply may borrow only from input. The runtime retains its
+ * backing until all reply consumers finish. Effect errors publish no reply. */
+ PureResult (*write)(void *, const void *definition, const char *,
+ int64_t expected, PureSpan input, PureSpan *reply,
+ int64_t *revision);
+} PureHost;
+typedef struct { PureResult status; int64_t revision; PureSpan body; } PureOutcome;
+
+/* Caller-provided work slots: no background thread or per-domain heap. These
+ * records are implementation storage, not an ABI or a wire representation. */
+/* Internal storage order packs the fixed key around small flags. This is not
+ * a public ABI; the layout is checked as part of the core cost gate. */
+struct PureWork {
+ PureTicket parent;
+ unsigned state;
+ PureInput input;
+ bool held;
+ char key[65];
+ int64_t expected;
+ PureOutcome outcome;
+ PureResource resource;
+ uint64_t generation;
+ size_t next, charged, pins;
+};
+struct PureRuntime {
+ const void *definition;
+ PureHost host;
+ PureWork *work;
+ size_t capacity, free_head, ready_head, ready_tail;
+ size_t budget, retained;
+#ifdef PURE_RUNTIME_METRICS
+ size_t peak_retained;
+#endif
+ size_t active;
+#ifdef PURE_RUNTIME_METRICS
+ size_t peak_active;
+#endif
+ uint64_t serial;
+#ifdef PURE_RUNTIME_METRICS
+ uint64_t accepted, commits, gets, conflicts, budget_rejections;
+ uint64_t releases, borrowed_output_bytes;
+#endif
+ bool accepting, running, async_effects;
+};
+
+/* Enforce repeated storage cost on 64-bit C hosts, including macOS where the
+ * Linux object-section checker is unavailable. */
+#if SIZE_MAX == UINT64_MAX && UINTPTR_MAX == UINT64_MAX
+_Static_assert(sizeof(PureWork) == 216, "work-slot layout exceeded its reviewed size");
+#ifdef PURE_RUNTIME_METRICS
+_Static_assert(sizeof(PureRuntime) == 192, "metrics domain layout changed");
+#else
+_Static_assert(sizeof(PureRuntime) == 120, "domain layout changed");
+#endif
+#endif
+
+/* Initialize fresh storage once. A NULL work pointer with capacity zero starts
+ * an idle domain; bind work before admission. Use restart, not init, to reuse
+ * a domain so its monotonic ticket sequence survives restart. definition is
+ * host-owned and remains alive until all work has drained. */
+bool pure_runtime_init(PureRuntime *, PureWork *, size_t capacity,
+ const void *definition, PureHost, size_t budget);
+bool pure_runtime_restart(PureRuntime *, const void *definition, PureHost, size_t budget);
+void pure_runtime_stop(PureRuntime *); /* stop admission, allow queued work to drain */
+/* Replace caller-owned work storage only after all tickets and loans retire.
+ * A zero-capacity binding releases idle slot storage without resetting budget,
+ * ticket generation or optional metrics. The caller retains the bound array's life. */
+bool pure_runtime_bind(PureRuntime *, PureWork *, size_t capacity);
+
+/* READ queues immediately. WRITE reserves its slot, budget and host allocation
+ * before returning. Fill ingress then publish once with ready. A partial input
+ * released without ready never executes. On error, out is unchanged. */
+PureResult pure_runtime_accept(PureRuntime *, PureInput, const char *key,
+ int64_t expected, size_t input_size, PureTicket *out);
+unsigned char *pure_runtime_ingress(PureTicket, size_t *size);
+bool pure_runtime_ready(PureTicket);
+bool pure_runtime_step(PureRuntime *); /* execute at most one ready job */
+bool pure_runtime_result(PureTicket, PureOutcome *out);
+
+/* Pin a completed reply and queue it as another domain's WRITE input, with no
+ * allocation, serialization or payload copy. Source may be released as soon
+ * as this succeeds; the core retains its backing until the consumer ends.
+ * Each domain charges its own retained input bytes as admission credits.
+ * Credits across domains are NOT distinct physical memory and must not be summed. */
+PureResult pure_runtime_forward(PureTicket source, PureRuntime *destination,
+ const char *key, int64_t expected, PureTicket *out);
+bool pure_runtime_cancel(PureTicket); /* only before execution; keeps a completion */
+bool pure_runtime_release(PureTicket); /* invalidates caller's ticket, preserves pins */
+
+#ifdef PURE_ASYNC_EFFECTS
+/* Optional owner-thread effect suspension. Enable before accepting work. The
+ * host may complete effects in any order, but must call complete on the owner
+ * thread after workers stop accessing its borrowed spans. An in-flight effect
+ * keeps its input alive even if the caller releases the ticket. Effects
+ * borrowed by take stay valid until completion. */
+typedef enum { PURE_TAKE_EMPTY, PURE_TAKE_EFFECT } PureTake;
+typedef struct {
+ PureTicket ticket;
+ PureInput input;
+ const char *key;
+ int64_t expected;
+ PureSpan value; /* raw ingress for WRITE; empty for READ */
+ size_t available; /* remaining payload credits for READ */
+} PureEffect;
+bool pure_runtime_enable_async(PureRuntime *);
+PureTake pure_runtime_take(PureRuntime *, PureEffect *);
+/* A valid completion consumes read_resource (even on failure); a rejected call
+ * leaves it untouched. write_reply borrows from the retained ingress. */
+bool pure_runtime_complete(PureTicket, PureResult status,
+ PureResource *read_resource, PureSpan write_reply,
+ int64_t revision);
+#endif
+#endif
diff --git a/core/state.h b/core/state.h
new file mode 100644
index 0000000..2bf78b0
--- /dev/null
+++ b/core/state.h
@@ -0,0 +1,84 @@
+#ifndef PURE_STATE_H
+#define PURE_STATE_H
+#include "flow.h"
+#include "result.h"
+#include <stdint.h>
+
+/* Runtime-owned working versions of stored values (selected tree store).
+ * A draft starts at the committed version of a key and changes only in
+ * memory: an edit copies the leaf and path it touches and shares every other
+ * extent with the stored version. An app therefore keeps no copy of the value
+ * and never compares a copy with storage to find what it changed. Commit
+ * appends only the draft's new extents. One owner thread per draft. */
+typedef struct PureDraft PureDraft;
+
+/* A working value with no stored base, for runtime-owned state such as an
+ * app's text. It edits and reads like any draft but cannot be committed. */
+PureDraft *pure_draft_new(void);
+/* A missing key opens as an empty draft at revision 0; commit creates it. */
+PureResult pure_draft_open(const char *path, const char *key, PureDraft **out);
+/* A runtime-owned value of an app, such as a list or text state, stored
+ * under a name that is never a document key (hosts listing documents do not
+ * see it). name is a valid key of at most 63 bytes. */
+PureResult pure_state_open(const char *path, const char *name, PureDraft **out);
+/* A store kept open by one owner thread. Drafts opened through it share one
+ * mapping of the newest version, and opening one checks only whether the file
+ * changed; commits of its drafts map the new version once. It must outlive
+ * its drafts. */
+typedef struct PureStore PureStore;
+PureResult pure_store_open(const char *path, PureStore **out);
+void pure_store_close(PureStore *);
+PureResult pure_store_draft(PureStore *, const char *key, PureDraft **out);
+PureResult pure_store_state(PureStore *, const char *name, PureDraft **out);
+/* Stored extents can outlive their draft: pure_draft_mapping takes a
+ * reference to the mapping a draft opened through a handle reads its stored
+ * nodes from (NULL for other drafts). Spans that mapping holds stay readable
+ * until the reference is released, after the draft closes and after the
+ * store moves on. Spans of a draft's unsaved edits are not in it. Take it on
+ * the handle's owner thread (a frozen draft's on any); the reference may be
+ * released on any thread. */
+typedef struct PureMapping PureMapping;
+PureMapping *pure_draft_mapping(const PureDraft *);
+bool pure_mapping_holds(const PureMapping *, PureSpan);
+void pure_mapping_release(PureMapping *);
+/* Pinned, drafts open at the version found when pinning, without checking
+ * the file each time (a host pins for one burst of requests); commits made
+ * through the handle still move it on. */
+PureResult pure_store_pin(PureStore *, bool pinned);
+/* Memory only. Fails without change on a bad range or when memory runs out. */
+PureResult pure_draft_splice(PureDraft *, size_t offset, size_t remove, PureSpan insert);
+size_t pure_draft_size(const PureDraft *);
+/* The committed revision this draft is based on; 0 if the key was absent. */
+int64_t pure_draft_revision(const PureDraft *);
+/* Visit [offset, offset + length) in order as borrowed extents, without
+ * copying. Returning false stops early. Spans stay valid until the next
+ * splice, commit or close of this draft. */
+PureResult pure_draft_read(const PureDraft *, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *context);
+/* An immutable version of a draft's value. It stays readable while the draft
+ * is held (reclamation is suspended) and until the next commit or close. */
+typedef struct { uint64_t ref, bytes; } PureVersion;
+PureVersion pure_draft_version(const PureDraft *);
+PureResult pure_draft_read_version(const PureDraft *, PureVersion, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *context);
+/* A read-only copy of a draft opened through a store handle, at its current
+ * version, when that version is all stored (no edits since its last commit,
+ * as right after one): it shares the draft's mapping, and since reading
+ * changes nothing it may be read, lend its mapping and close on any thread.
+ * NULL otherwise, or when memory runs out. */
+PureDraft *pure_draft_freeze(const PureDraft *);
+/* While held, edits keep every earlier version's nodes (one transaction). */
+void pure_draft_hold(PureDraft *, bool held);
+/* Abandon edits made since version was taken; the draft was held since then. */
+void pure_draft_restore(PureDraft *, PureVersion);
+/* Publish if the key is still at the draft's base revision. On success the
+ * draft continues from the new revision; on any failure it is unchanged. */
+PureResult pure_draft_commit(PureDraft *, int64_t *revision);
+/* Publish drafts of one store in one commit: each key must still be at its
+ * draft's base revision (a conflict names the stored revision in revisions),
+ * and either every draft is published or none is. Keys must differ. */
+PureResult pure_draft_commit_all(PureDraft *const *drafts, size_t count, int64_t *revisions);
+/* Commits made so far in this process: groups formed, and requests in them. */
+void pure_store_group_counts(unsigned long *groups, unsigned long *requests);
+void pure_draft_close(PureDraft *);
+#endif
diff --git a/core/store.h b/core/store.h
new file mode 100644
index 0000000..dbfa8f8
--- /dev/null
+++ b/core/store.h
@@ -0,0 +1,54 @@
+#ifndef PURE_STORE_H
+#define PURE_STORE_H
+#include "flow.h"
+#include "result.h"
+#include <stdint.h>
+
+#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u)
+typedef struct {
+ void *mapping;
+ size_t mapped_size, object_size;
+ PureSpan body;
+ int64_t revision;
+} PureStoredView;
+
+PureResult pure_store_init(const char *path);
+PureResult pure_store_get(const char *path, const char *key, size_t available,
+ PureStoredView *out);
+PureResult pure_store_get_range(const char *path, const char *key, size_t offset,
+ size_t length, size_t available, PureStoredView *out);
+/* Inspect once: guards and mapping refer to the same immutable file version.
+ * minimum preserves bounds checks from eliminated intermediate views.
+ * whole selects the suffix starting at offset; otherwise length is exact. */
+PureResult pure_store_select(const char *path, const char *key, size_t offset,
+ size_t length, bool whole, size_t minimum,
+ size_t available, PureStoredView *out);
+void pure_store_release(PureStoredView *);
+PureResult pure_store_put(const char *path, const char *key, int64_t expected,
+ PureSpan body, int64_t *revision);
+/* Atomic byte replacement: retain [0,offset), insert bytes, retain suffix after
+ * remove bytes. Check the expected revision and bounds under the same writer
+ * lock. expected=0 permits creating an empty base (offset=remove=0).
+ * Retained bytes use bounded scratch space or a selected platform file copy;
+ * never assembles a whole new object in application memory. The immutable
+ * file is still rewritten. max_size is the final-body policy from the
+ * prepared app and is checked with the current size under the writer lock. */
+PureResult pure_store_splice(const char *path, const char *key, int64_t expected,
+ size_t offset, size_t remove, PureSpan insert,
+ size_t max_size, int64_t *revision);
+
+/* Visit each stored key once; the order is store-defined. Returning false
+ * from visit stops early. key is valid only during the call. Hosts that
+ * browse documents use this instead of reading a store's file layout. */
+PureResult pure_store_keys(const char *path, bool (*visit)(void *, const char *key),
+ void *context);
+
+/* Fault hooks exist only in the separately built unit-test binary. */
+typedef enum {
+ PURE_AFTER_HEADER, PURE_BEFORE_FILE_SYNC, PURE_BEFORE_RENAME, PURE_BEFORE_DIR_SYNC,
+ PURE_COPY_RANGE_UNSUPPORTED_AFTER_PARTIAL
+} PureStoreStage;
+#ifdef PURE_STORE_TEST
+int pure_store_test_fault(PureStoreStage stage);
+#endif
+#endif
diff --git a/core/test-module.c b/core/test-module.c
new file mode 100644
index 0000000..ccb3371
--- /dev/null
+++ b/core/test-module.c
@@ -0,0 +1,2853 @@
+#define _POSIX_C_SOURCE 200809L
+#include "module.h"
+#include "module-store.h"
+#include "module-text.h"
+#include "store.h"
+#include <dirent.h>
+#include <math.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define CHECK(x) do { if (!(x)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); abort(); } } while (0)
+
+/* Store faults: a stage fails (or the process dies there when crash is set). */
+static int fail_stage = -1;
+static int crash;
+int pure_store_test_fault(PureStoreStage stage) {
+ if ((int)stage != fail_stage) return 0;
+ if (crash) _exit(77);
+ return 1;
+}
+
+typedef struct { const char *name; double initial; } S;
+typedef struct { const char *name; uint32_t type; } F;
+/* kind 0 is a key input; otherwise PURE_KIND_MESSAGE with fields. */
+typedef struct { const char *name, *key; int kind; bool keyed; const F *fields; size_t nfields; } I;
+#define KEY(name, key) {name, key, 0, false, NULL, 0}
+/* A timer's period (milliseconds) rides in nfields. */
+#define TIMER(name, period) {name, NULL, PURE_KIND_TIMER, false, NULL, period}
+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), 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; /* type: the state type, its flags from bit 8 */
+/* 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;
+/* A further view: a table (type 2, start = list state) or a column of the
+ * view numbered table - 1. */
+typedef struct { const char *name; uint32_t type, table, start, length; } W;
+typedef struct {
+ const S *s; size_t ns; const I *in; size_t ni; const V *v; size_t nv;
+ const H *h; size_t nh; const O *o; size_t no; const char *const *c; size_t nc;
+ const T *t; size_t nt; const char *const *k; size_t nk; const V *tv; size_t ntv;
+ const R *r; size_t nr; const W *w; size_t nw;
+} Spec;
+
+static unsigned char *put32(unsigned char *p, uint32_t v) {
+ for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(v >> (8 * i));
+ return p + 4;
+}
+
+static unsigned char *put_double(unsigned char *p, double value) {
+ uint64_t bits;
+ memcpy(&bits, &value, sizeof(bits));
+ put32(p, (uint32_t)bits);
+ return put32(p + 4, (uint32_t)(bits >> 32));
+}
+
+/* Test-side encoder for format v5; the toolchain has its own. */
+static size_t build(unsigned char *out, Spec spec) {
+ unsigned char strings[4096], blob[4096];
+ size_t used = 0, blob_used = 0;
+ const char *text[512];
+ uint32_t offset[512];
+ size_t names = 0;
+ for (size_t i = 0; i < spec.ns; ++i) text[names++] = spec.s[i].name;
+ for (size_t i = 0; i < spec.nt; ++i) text[names++] = spec.t[i].name;
+ for (size_t i = 0; i < spec.nc; ++i) text[names++] = spec.c[i];
+ for (size_t i = 0; i < spec.ni; ++i) {
+ text[names++] = spec.in[i].name;
+ if (!spec.in[i].kind) text[names++] = spec.in[i].key;
+ for (size_t f = 0; spec.in[i].kind != PURE_KIND_TIMER && f < spec.in[i].nfields; ++f)
+ text[names++] = spec.in[i].fields[f].name;
+ }
+ for (size_t i = 0; i < spec.nv; ++i) text[names++] = spec.v[i].name;
+ for (size_t i = 0; i < spec.ntv; ++i) text[names++] = spec.tv[i].name;
+ for (size_t i = 0; i < spec.nw; ++i) text[names++] = spec.w[i].name;
+ size_t record_names = names, record_fields = 0;
+ for (size_t i = 0; i < spec.nr; ++i) {
+ text[names++] = spec.r[i].name;
+ 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;
+ strings[used++] = (unsigned char)length;
+ memcpy(strings + used, text[i], length);
+ used += length;
+ }
+ size_t fields = 0;
+ for (size_t i = 0; i < spec.ni; ++i) fields += spec.in[i].kind == PURE_KIND_TIMER ? 0 : spec.in[i].nfields;
+ 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, "PUREMOD5", 8);
+ size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv + spec.nw;
+ p[8] = (unsigned char)states; p[9] = (unsigned char)(states >> 8);
+ p[10] = (unsigned char)spec.ni; p[12] = (unsigned char)views; p[14] = (unsigned char)spec.nh;
+ put32(p + 16, (uint32_t)spec.no);
+ 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;
+ for (size_t i = 0; i < spec.ns; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put_double(p, spec.s[i].initial); }
+ for (size_t i = 0; i < spec.nt; ++i) {
+ p = put32(p, offset[n++]);
+ p = put32(p, spec.t[i].type & 0xFF);
+ p = put32(p, spec.t[i].ref);
+ p = put32(p, spec.t[i].type >> 8);
+ }
+ for (size_t i = 0; i < spec.nc; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, 1); }
+ uint32_t field_offsets[64], field_start = 0;
+ size_t nf = 0;
+ for (size_t i = 0; i < spec.ni; ++i) {
+ const I *in = &spec.in[i];
+ p = put32(p, offset[n++]);
+ *p++ = (unsigned char)(in->kind ? in->kind : 1); *p++ = in->keyed; *p++ = 0; *p++ = 0;
+ bool timer = in->kind == PURE_KIND_TIMER;
+ p = put32(p, timer ? (uint32_t)in->nfields : in->kind ? 0 : offset[n++]);
+ uint32_t first = in->kind == PURE_KIND_MESSAGE || in->kind == PURE_KIND_OUTPUT ? field_start : 0; /* only messages and outputs have fields */
+ *p++ = (unsigned char)first; *p++ = (unsigned char)(first >> 8);
+ *p++ = (unsigned char)(timer ? 0 : in->nfields); *p++ = 0;
+ for (size_t f = 0; !timer && f < in->nfields; ++f) field_offsets[nf++] = offset[n++];
+ field_start += timer ? 0 : (uint32_t)in->nfields;
+ }
+ nf = 0;
+ for (size_t i = 0; i < spec.ni; ++i)
+ for (size_t f = 0; spec.in[i].kind != PURE_KIND_TIMER && f < spec.in[i].nfields; ++f) {
+ p = put32(p, field_offsets[nf++]);
+ 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.nw; ++i) {
+ const W *w = &spec.w[i];
+ p = put32(p, offset[n++]);
+ *p++ = (unsigned char)w->type; *p++ = (unsigned char)(w->type >> 8); /* a filtered table: its list + 1 */
+ *p++ = (unsigned char)w->table; *p++ = (unsigned char)(w->table >> 8);
+ p = put32(p, w->start);
+ p = put32(p, w->length);
+ }
+ for (size_t i = 0; i < spec.nh; ++i) { p = put32(p, spec.h[i].event); p = put32(p, spec.h[i].start); p = put32(p, spec.h[i].length); }
+ for (size_t i = 0; i < spec.no; ++i) {
+ *p++ = spec.o[i].op; *p++ = 0; *p++ = 0; *p++ = 0;
+ p = put32(p, spec.o[i].arg);
+ p = put_double(p, spec.o[i].value);
+ }
+ memcpy(p, strings, used);
+ memcpy(p + used, blob, blob_used);
+ return (size_t)(p + used + blob_used - out);
+}
+
+static PureModule module_a, module_b;
+static char error[128];
+
+static PureResult load(Spec spec, PureModule *out) {
+ static unsigned char bytes[65536];
+ size_t size = build(bytes, spec);
+ return pure_module_load(bytes, size, out, error, sizeof(error));
+}
+
+#define LEN(a) (sizeof(a) / sizeof(*(a)))
+#define SPEC(s, in, v, h, o) ((Spec){s, LEN(s), in, LEN(in), v, LEN(v), h, LEN(h), o, LEN(o), NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, 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,
+ MAX = PURE_OP_MAX, CL = PURE_OP_CLAMP, ST = PURE_OP_STORE, JZ = PURE_OP_JZ,
+ JMP = PURE_OP_JMP, NEG = PURE_OP_NEG };
+
+static const S counter_states[] = {{"count", 0}, {"x", 3}};
+static const I counter_inputs[] = {KEY("tap", "Space"), KEY("move", "ArrowRight")};
+static const V counter_views[] = {{"label", 0, 1}, {"width", 1, 6}};
+static const O counter_code[] = {
+ {L, 0, 0}, /* 0 view label */
+ {C, 0, 40}, {L, 0, 0}, {C, 0, 20}, {MIN, 0, 0}, {C, 0, 8}, /* 1 view width */
+ {MUL, 0, 0},
+ /* 7: fixed-up below: 40 + min(count, 20) * 8 needs an ADD; see width2 */
+ {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 7 tap.down */
+ /* 11 move.down: if x > 5 { x = 0 } else { x = x + 1 } */
+ {L, 1, 0}, {C, 0, 5}, {GT, 0, 0}, {JZ, 18, 0}, {C, 0, 0}, {ST, 1, 0}, {JMP, 22, 0},
+ {L, 1, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 1, 0},
+ /* 22 frame: x = x + dt */
+ {L, 1, 0}, {DT, 0, 0}, {ADD, 0, 0}, {ST, 1, 0},
+ /* 26 view width2 = 40 + min(count, 20) * 8 */
+ {C, 0, 40}, {L, 0, 0}, {C, 0, 20}, {MIN, 0, 0}, {C, 0, 8}, {MUL, 0, 0}, {ADD, 0, 0},
+};
+static const H counter_handlers[] = {{1, 7, 4}, {3, 11, 11}, {0, 22, 4}};
+
+static void behaviour(void) {
+ /* width at 1..7 leaves two values: rejected at load. */
+ CHECK(load(SPEC(counter_states, counter_inputs, counter_views, counter_handlers, counter_code), &module_a) == PURE_INVALID);
+ CHECK(strstr(error, "view must leave one value of its type"));
+ static const V views[] = {{"label", 0, 1}, {"width", 26, 7}};
+ CHECK(load(SPEC(counter_states, counter_inputs, views, counter_handlers, counter_code), &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ double value;
+ CHECK(pure_app_view(&app, 1, &value) && value == 40);
+ for (int i = 0; i < 25; ++i) CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.values[0] == 25 && pure_app_view(&app, 1, &value) && value == 200);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_UP) == PURE_OK && app.events == 26);
+ /* if / else with forward JZ and JMP */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 4);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 5);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 6);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 0);
+ CHECK(pure_app_frame(&app) == PURE_OK && app.values[1] == 1.0 / 60.0 && app.ticks == 1);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_INVALID);
+ CHECK(pure_module_input(&module_a, "move") == 1 && pure_module_view(&module_a, "width") == 1 &&
+ pure_module_state(&module_a, "missing") == -1);
+
+ /* Replacement keeps values by name. */
+ static const S next_states[] = {{"fresh", 7}, {"count", 99}};
+ static const I no_inputs[1];
+ static const V no_views[1];
+ static const H no_handlers[1];
+ static const O no_code[1];
+ CHECK(load((Spec){next_states, 2, no_inputs, 0, no_views, 0, no_handlers, 0, no_code, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, 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);
+ CHECK(app.values[0] == 25 && app.values[1] == 3);
+ pure_app_stop(&app);
+}
+
+/* A fault leaves every state exactly as before the event. */
+static void faults(void) {
+ static const S states[] = {{"a", 1}, {"b", 2}};
+ static const I inputs[] = {KEY("one", "KeyA"), KEY("two", "KeyB"), KEY("three", "Digit3")};
+ static const O code[] = {
+ {C, 0, 9}, {ST, 0, 0}, {C, 0, 1}, {C, 0, 0}, {DIV, 0, 0}, {ST, 1, 0}, /* 0 divide */
+ {C, 0, 9}, {ST, 0, 0}, {C, 0, 1e308}, {C, 0, 10}, {MUL, 0, 0}, {ST, 1, 0}, /* 6 overflow */
+ {C, 0, 9}, {ST, 0, 0}, {C, 0, 1}, {C, 0, 3}, {C, 0, 2}, {CL, 0, 0}, {ST, 1, 0}, /* 12 clamp */
+ {C, 0, 5}, {ST, 0, 0}, /* 19 ok */
+ {C, 0, 0}, {NEG, 0, 0}, {C, 0, 0}, {MIN, 0, 0}, /* 21 view -0 */
+ {C, 0, 0}, {C, 0, 0}, {NEG, 0, 0}, {MAX, 0, 0}, /* 25 view +0 */
+ };
+ static const H handlers[] = {{1, 0, 6}, {3, 6, 6}, {5, 12, 7}, {5, 19, 2}};
+ static const V views[] = {{"low", 21, 4}, {"high", 25, 4}};
+ CHECK(load(SPEC(states, inputs, views, handlers, code), &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ const PureFault expected[] = {PURE_FAULT_DIVIDE, PURE_FAULT_NON_FINITE, PURE_FAULT_CLAMP};
+ for (uint32_t i = 0; i < 3; ++i) {
+ CHECK(pure_app_input(&app, i, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == expected[i]);
+ CHECK(app.values[0] == 1 && app.values[1] == 2 && app.events == 0);
+ }
+ /* A later handler of the same event does not run after a fault. */
+ CHECK(app.fault_pc == 17);
+ double value;
+ CHECK(pure_app_view(&app, 0, &value) && value == 0 && signbit(value));
+ CHECK(pure_app_view(&app, 1, &value) && value == 0 && !signbit(value));
+ pure_app_stop(&app);
+}
+
+typedef struct { const char *reason; Spec spec; PureResult result; } Rejection;
+
+static void rejections(void) {
+ static const S s1[] = {{"a", 0}};
+ static const S dup[] = {{"a", 0}, {"a", 1}};
+ static const S bad_name[] = {{"1a", 0}};
+ static const S long_name[] = {{"abcdefghijklmnopqrstuvwxyz012345", 0}};
+ static const S non_finite[] = {{"a", INFINITY}};
+ static const I i1[] = {KEY("k", "Space")};
+ static const I bad_key[] = {KEY("k", "KeyAA")};
+ static const I dup_key[] = {KEY("k", "Space"), KEY("j", "Space")};
+ static const V clash[] = {{"a", 0, 1}};
+ static const V v_store[] = {{"v", 0, 2}};
+ static const V v_dt[] = {{"v", 2, 1}};
+ static const V v_empty[] = {{"v", 0, 0}};
+ static const H none[1];
+ static const O load_a[] = {{L, 0, 0}};
+ static const O store[] = {{C, 0, 1}, {ST, 0, 0}, {DT, 0, 0}};
+ static const H h0[] = {{0, 0, 2}};
+ static const H h_bad_event[] = {{3, 0, 2}};
+ static const O back[] = {{C, 0, 1}, {JZ, 0, 0}};
+ static const O beyond[] = {{C, 0, 1}, {JZ, 3, 0}};
+ static const O under[] = {{ADD, 0, 0}};
+ static const O join[] = {{C, 0, 1}, {JZ, 4, 0}, {C, 0, 1}, {C, 0, 1}, {ST, 0, 0}};
+ static const O leftover[] = {{C, 0, 1}, {C, 0, 2}};
+ static const O unknown_state[] = {{L, 1, 0}, {ST, 0, 0}};
+ static const O nan_const[] = {{C, 0, NAN}, {ST, 0, 0}};
+ static const O noncanon[] = {{C, 0, 1}, {NEG, 1, 0}, {ST, 0, 0}};
+ static const O unreachable[] = {{JMP, 2, 0}, {C, 0, 1}, {C, 0, 1}, {ST, 0, 0}};
+ static const O bad_op[] = {{99, 0, 0}, {ST, 0, 0}};
+ static const H h_all[] = {{0, 0, 1}};
+ static const H h_two[] = {{0, 0, 2}};
+ static const H h_five[] = {{0, 0, 5}};
+ static const H h_four[] = {{0, 0, 4}};
+ O overflow[66];
+ for (int i = 0; i < 65; ++i) overflow[i] = (O){C, 0, 1};
+ overflow[65] = (O){ST, 0, 0};
+ const Rejection cases[] = {
+ {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"state name", {bad_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"state name", {long_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"non-finite initial value", {non_finite, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"input key", {s1, 1, bad_key, 1, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"duplicate key", {s1, 1, dup_key, 2, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"duplicate name", {s1, 1, i1, 0, clash, 1, none, 0, load_a, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"view with an effect", {s1, 1, i1, 0, v_store, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"view with an effect", {s1, 1, i1, 0, v_dt, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"empty view", {s1, 1, i1, 0, v_empty, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"handler event", {s1, 1, i1, 1, clash, 0, h_bad_event, 1, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, back, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, beyond, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"stack underflow", {s1, 1, i1, 0, clash, 0, h_all, 1, under, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"join with different stacks", {s1, 1, i1, 0, clash, 0, h_five, 1, join, 5, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"handler must leave no value", {s1, 1, i1, 0, clash, 0, h_two, 1, leftover, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"no such state", {s1, 1, i1, 0, clash, 0, h_two, 1, unknown_state, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h_two, 1, nan_const, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h0, 1, noncanon, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"unreachable instruction", {s1, 1, i1, 0, clash, 0, h_four, 1, unreachable, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"unknown opcode", {s1, 1, i1, 0, clash, 0, h_two, 1, bad_op, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"stack overflow", {s1, 1, i1, 0, clash, 0, (H[]){{0, 0, 66}}, 1, overflow, 66, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ };
+ for (size_t i = 0; i < LEN(cases); ++i) {
+ PureResult result = load(cases[i].spec, &module_a);
+ if (result != cases[i].result || !strstr(error, cases[i].reason)) {
+ fprintf(stderr, "case %zu: expected '%s', got %d '%s'\n", i, cases[i].reason, (int)result, error);
+ abort();
+ }
+ }
+ static S many[33];
+ static char many_names[33][8];
+ for (unsigned i = 0; i < 33; ++i) { snprintf(many_names[i], 8, "s%u", i % 100u); many[i] = (S){many_names[i], 0}; }
+ CHECK(load((Spec){many, 33, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_a) == PURE_LIMIT);
+ static unsigned char bytes[65536];
+ size_t size = build(bytes, (Spec){s1, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, 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';
+ CHECK(pure_module_load(bytes, size, &module_a, error, sizeof(error)) == PURE_INVALID);
+}
+
+static uint32_t next_random(uint32_t *state) {
+ *state ^= *state << 13;
+ *state ^= *state >> 17;
+ *state ^= *state << 5;
+ return *state;
+}
+
+/* Arbitrary damage is either rejected or yields a module that runs safely. */
+static void mutations(void) {
+ static const V views[] = {{"label", 0, 1}, {"width", 26, 7}};
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, SPEC(counter_states, counter_inputs, views, counter_handlers, counter_code));
+ uint32_t state = 0x2545f491u;
+ unsigned accepted = 0;
+ for (unsigned round = 0; round < 20000; ++round) {
+ memcpy(bytes, valid, size);
+ unsigned flips = 1 + next_random(&state) % 4;
+ 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_a, error, sizeof(error)) != PURE_OK) continue;
+ ++accepted;
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ for (unsigned e = 0; e < 16; ++e) {
+ if (e % 3 == 0) pure_app_frame(&app);
+ else pure_app_input(&app, e % 4, e & 1 ? PURE_EDGE_UP : PURE_EDGE_DOWN);
+ for (uint32_t v = 0; v < module_a.views; ++v) { double value; pure_app_view(&app, v, &value); }
+ for (uint32_t s = 0; s < module_a.states; ++s) CHECK(isfinite(app.values[s]));
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
+static Spec editor_spec;
+
+/* An in-memory data host: every splice makes a new immutable version. */
+typedef struct { char key[65]; unsigned char *bytes; size_t size; int64_t revision; } Stored;
+typedef struct {
+ Stored docs[8];
+ size_t count, limit;
+ bool busy;
+ unsigned opened, closed;
+} Memory;
+typedef struct {
+ Memory *memory;
+ int index; /* -1 for an absent key */
+ int64_t base;
+ unsigned char *versions[64];
+ size_t sizes[64], count;
+} Doc;
+
+static Stored *stored(Memory *m, const char *key) {
+ for (size_t i = 0; i < m->count; ++i) if (!strcmp(m->docs[i].key, key)) return &m->docs[i];
+ return NULL;
+}
+
+static PureResult fake_open(void *context, uint32_t collection, const char *key, void **out, int64_t *revision) {
+ Memory *m = context;
+ CHECK(collection == 0);
+ Doc *d = calloc(1, sizeof(*d));
+ Stored *s = stored(m, key);
+ d->memory = m;
+ d->index = s ? (int)(s - m->docs) : -1;
+ if (!s) {
+ CHECK(m->count < 8);
+ d->index = (int)m->count;
+ snprintf(m->docs[m->count++].key, 65, "%s", key);
+ s = &m->docs[d->index];
+ }
+ d->base = s->revision;
+ d->versions[0] = malloc(s->size + 1);
+ memcpy(d->versions[0], s->bytes ? s->bytes : (unsigned char *)"", s->size);
+ d->sizes[0] = s->size;
+ d->count = 1;
+ ++m->opened;
+ *out = d;
+ *revision = s->revision;
+ return PURE_OK;
+}
+
+static PureVersion fake_version(void *context, void *document) {
+ (void)context;
+ Doc *d = document;
+ return (PureVersion){d->count - 1, d->sizes[d->count - 1]};
+}
+
+/* Two spans per read, so replies arrive in pieces. */
+static PureResult fake_read(void *context, void *document, PureVersion at, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *visit_context) {
+ (void)context;
+ Doc *d = document;
+ CHECK(at.ref < d->count && at.bytes == d->sizes[at.ref] && offset + length <= at.bytes);
+ size_t half = length / 2;
+ if (half && !visit(visit_context, (PureSpan){d->versions[at.ref] + offset, half})) return PURE_OK;
+ if (length - half) visit(visit_context, (PureSpan){d->versions[at.ref] + offset + half, length - half});
+ return PURE_OK;
+}
+
+static PureResult fake_splice(void *context, void *document, size_t offset, size_t remove, PureSpan insert) {
+ Memory *m = context;
+ Doc *d = document;
+ size_t size = d->sizes[d->count - 1], next = size - remove + insert.size;
+ if (next > m->limit) return PURE_LIMIT;
+ CHECK(d->count < 64);
+ unsigned char *bytes = malloc(next + 1);
+ memcpy(bytes, d->versions[d->count - 1], offset);
+ if (insert.size) memcpy(bytes + offset, insert.data, insert.size);
+ memcpy(bytes + offset + insert.size, d->versions[d->count - 1] + offset + remove, size - offset - remove);
+ d->versions[d->count] = bytes;
+ d->sizes[d->count++] = next;
+ return PURE_OK;
+}
+
+static PureResult fake_commit(void *context, void *document, int64_t *revision) {
+ Memory *m = context;
+ Doc *d = document;
+ Stored *s = &m->docs[d->index];
+ *revision = s->revision;
+ if (m->busy) return PURE_BUSY;
+ if (s->revision != d->base) return PURE_CONFLICT;
+ free(s->bytes);
+ s->size = d->sizes[d->count - 1];
+ s->bytes = malloc(s->size + 1);
+ memcpy(s->bytes, d->versions[d->count - 1], s->size);
+ *revision = ++s->revision;
+ /* Like a tree draft, a commit rebases: earlier versions are gone. */
+ for (size_t i = 0; i + 1 < d->count; ++i) { free(d->versions[i]); d->versions[i] = NULL; d->sizes[i] = 0; }
+ d->versions[0] = d->versions[d->count - 1];
+ d->sizes[0] = d->sizes[d->count - 1];
+ d->count = 1;
+ d->base = s->revision;
+ return PURE_OK;
+}
+
+static void fake_close(void *context, void *document) {
+ Memory *m = context;
+ Doc *d = document;
+ for (size_t i = 0; i < d->count; ++i) free(d->versions[i]);
+ free(d);
+ ++m->closed;
+}
+
+static PureData fake_data(Memory *m) {
+ return (PureData){m, fake_open, fake_version, fake_read, fake_splice, fake_commit, fake_close, NULL, NULL};
+}
+
+typedef struct { unsigned char bytes[256]; size_t size, spans; } Collected;
+
+static bool count_bytes(void *context, PureSpan span) {
+ *(size_t *)context += span.size;
+ return true;
+}
+
+static bool collect(void *context, PureSpan span) {
+ Collected *c = context;
+ CHECK(c->size + span.size <= sizeof(c->bytes));
+ memcpy(c->bytes + c->size, span.data, span.size);
+ c->size += span.size;
+ ++c->spans;
+ return true;
+}
+
+static bool replied(const PureApp *app, const char *text) {
+ Collected c = {{0}, 0, 0};
+ CHECK(pure_app_reply(app, collect, &c) == PURE_OK);
+ return c.size == strlen(text) && !memcmp(c.bytes, text, c.size);
+}
+
+static PureSpan edit_payload(unsigned char *buffer, uint32_t start, uint32_t remove, const char *insert) {
+ put32(buffer, start);
+ put32(buffer + 4, remove);
+ memcpy(buffer + 8, insert, strlen(insert));
+ return (PureSpan){buffer, 8 + strlen(insert)};
+}
+
+enum { FIELD = PURE_OP_FIELD, DOC = PURE_OP_DOC, SIZE = PURE_OP_DOC_SIZE, LEN_ = PURE_OP_LEN,
+ REPL = PURE_OP_REPLACE, REPLY = PURE_OP_REPLY, REQ = PURE_OP_REQUIRE, LE = PURE_OP_LE };
+
+/* The document editor as a module: show replies the document, edit splices
+ * start .. start + remove with insert, replies insert and counts edits. A
+ * keep message replies the document read before its own edit; guarded edits
+ * require the final size to stay within 8 bytes. */
+static void messages(void) {
+ static const F edit_fields[] = {{"start", 1}, {"remove", 1}, {"insert", 2}};
+ static const I inputs[] = {
+ {"show", NULL, PURE_KIND_MESSAGE, true, NULL, 0},
+ {"edit", NULL, PURE_KIND_MESSAGE, true, edit_fields, 3},
+ {"keep", NULL, PURE_KIND_MESSAGE, true, edit_fields + 2, 1},
+ {"guarded", NULL, PURE_KIND_MESSAGE, true, edit_fields, 3},
+ KEY("tick", "Space"),
+ };
+ static const S states[] = {{"edits", 0}};
+ static const char *const collections[] = {"documents"};
+ static const O code[] = {
+ {DOC, 0, 0}, {REPLY, 0, 0}, /* 0 show */
+ {FIELD, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0}, {ADD, 0, 0}, {FIELD, 2, 0}, /* 2 edit */
+ {REPL, 0, 0}, {FIELD, 2, 0}, {REPLY, 0, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0},
+ {DOC, 0, 0}, {REPLY, 0, 0}, {C, 0, 0}, {C, 0, 0}, {FIELD, 0, 0}, {REPL, 0, 0}, /* 14 keep: prepend */
+ {FIELD, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0}, {ADD, 0, 0}, {FIELD, 2, 0}, /* 20 guarded */
+ {REPL, 0, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0},
+ {SIZE, 0, 0}, {C, 0, 8}, {LE, 0, 0}, {REQ, 0, 0},
+ };
+ static const H handlers[] = {{1, 0, 2}, {3, 2, 12}, {5, 14, 6}, {7, 20, 14}};
+ static const V views[] = {{"count", 0, 0}};
+ Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ editor_spec = spec;
+ PureResult loaded = load(spec, &module_a);
+ if (loaded != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ Memory memory = {.limit = 100};
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, fake_data(&memory)) == PURE_OK);
+ unsigned char buffer[64];
+
+ CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_NOT_FOUND && app.revision == 0);
+ CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "hello")) == PURE_OK);
+ CHECK(app.revision == 1 && replied(&app, "hello") && app.values[0] == 1);
+ CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && app.revision == 1 && replied(&app, "hello"));
+ /* stale base conflicts before bounds; bounds fail as limit; nothing changes */
+ CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 99, 0, "x")) == PURE_CONFLICT && app.revision == 1);
+ CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 99, 0, "x")) == PURE_LIMIT && app.revision == 1);
+ CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 2, 9, "x")) == PURE_LIMIT);
+ CHECK(pure_app_message(&app, 1, "doc", 1, (PureSpan){buffer, 7}) == PURE_LIMIT && app.revision == 0);
+ CHECK(pure_app_message(&app, 1, "doc", -1, edit_payload(buffer, 0, 0, "x")) == PURE_INVALID);
+ CHECK(pure_app_message(&app, 1, "bad/key", 1, edit_payload(buffer, 0, 0, "x")) == PURE_INVALID);
+ CHECK(pure_app_message(&app, 4, "doc", 1, (PureSpan){0}) == PURE_INVALID);
+ CHECK(memory.docs[0].revision == 1 && memory.docs[0].size == 5 && app.values[0] == 1);
+ /* A busy commit changes neither the document nor the states. */
+ memory.busy = true;
+ CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 5, 0, "!")) == PURE_BUSY);
+ memory.busy = false;
+ CHECK(memory.docs[0].size == 5 && app.values[0] == 1 && app.events == 2);
+ /* The size limit of the data host fails as limit. */
+ memory.limit = 6;
+ CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 5, 0, "!!")) == PURE_LIMIT);
+ memory.limit = 100;
+ /* A reply read before the event's own edit keeps its content after commit. */
+ CHECK(pure_app_message(&app, 2, "doc", 1, (PureSpan){(const unsigned char *)">> ", 3}) == PURE_OK);
+ CHECK(app.revision == 2 && replied(&app, "hello"));
+ CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && replied(&app, ">> hello"));
+ /* require: an edit that would exceed 8 bytes changes nothing */
+ CHECK(pure_app_message(&app, 3, "doc", 2, edit_payload(buffer, 8, 0, "!")) == PURE_LIMIT);
+ CHECK(memory.docs[0].size == 8 && app.values[0] == 1);
+ CHECK(pure_app_message(&app, 3, "doc", 2, edit_payload(buffer, 0, 3, "")) == PURE_OK && app.revision == 3);
+ CHECK(app.values[0] == 2);
+ /* Another key is its own document. */
+ CHECK(pure_app_message(&app, 1, "other", 0, edit_payload(buffer, 0, 0, "abc")) == PURE_OK && app.revision == 1);
+ pure_app_stop(&app);
+ CHECK(memory.opened == memory.closed);
+ for (size_t i = 0; i < memory.count; ++i) free(memory.docs[i].bytes);
+
+ /* Verifier rules for typed operations. */
+ static const O field_in_key[] = {{FIELD, 0, 0}, {ST, 0, 0}};
+ static const H key_handler[] = {{9, 0, 2}};
+ Spec bad = spec;
+ bad.o = field_in_key; bad.no = 2; bad.h = key_handler; bad.nh = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message operation outside"));
+ static const O num_insert[] = {{C, 0, 0}, {C, 0, 0}, {C, 0, 1}, {REPL, 0, 0}};
+ static const H edit_handler[] = {{3, 0, 4}};
+ bad.o = num_insert; bad.no = 4; bad.h = edit_handler;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ static const O bytes_state[] = {{FIELD, 2, 0}, {ST, 0, 0}};
+ static const H edit_handler2[] = {{3, 0, 2}};
+ bad.o = bytes_state; bad.no = 2; bad.h = edit_handler2;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ static const O far_field[] = {{FIELD, 3, 0}, {REPLY, 0, 0}};
+ bad.o = far_field; bad.h = edit_handler2;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state, field"));
+ static const I unkeyed[] = {{"note", NULL, PURE_KIND_MESSAGE, false, NULL, 0}};
+ static const O doc_read[] = {{DOC, 0, 0}, {REPLY, 0, 0}};
+ static const H note_handler[] = {{1, 0, 2}};
+ bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "document operation outside"));
+ static const F unknown_type[] = {{"insert", 5}, {"start", 1}};
+ static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, true, unknown_type, 2}};
+ bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message field"));
+ static const H even[] = {{2, 0, 0}};
+ bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "handler event"));
+ static const V field_view[] = {{"v", 0, 1}};
+ static const O field_only[] = {{FIELD, 0, 0}};
+ bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
+ /* A value may cross a join when every path brings the same stack. */
+ static const O branch_value[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 3, 0}, {ST, 0, 0}};
+ static const O branch_mismatch[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 4, 0}, {C, 0, 2}, {ST, 0, 0}};
+ static const H frame_handler[] = {{0, 0, 4}};
+ bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_OK);
+ static const H frame_five[] = {{0, 0, 5}};
+ bad.o = branch_mismatch; bad.no = 5; bad.h = frame_five;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks"));
+}
+
+/* Damaged message modules either fail to load or run messages safely. */
+static void message_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, editor_spec);
+ uint32_t state = 0x9e3779b9u;
+ 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;
+ Memory memory = {.limit = 64};
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_b, fake_data(&memory)) == PURE_OK);
+ unsigned char payload[32];
+ for (unsigned e = 0; e < 12; ++e) {
+ for (size_t k = 0; k < sizeof(payload); ++k) payload[k] = (unsigned char)next_random(&state);
+ put32(payload, next_random(&state) % 12);
+ put32(payload + 4, next_random(&state) % 4);
+ PureSpan span = {payload, next_random(&state) % sizeof(payload)};
+ uint32_t input = next_random(&state) % 5;
+ if (module_b.input[input].kind == PURE_KIND_MESSAGE) {
+ int64_t base = (int64_t)(next_random(&state) % 4) - 1;
+ if (pure_app_message(&app, input, "doc", base, span) == PURE_OK) {
+ size_t reply = 0;
+ CHECK(pure_app_reply(&app, count_bytes, &reply) == PURE_OK && reply <= 256);
+ }
+ } else pure_app_input(&app, input, e & 1 ? PURE_EDGE_UP : PURE_EDGE_DOWN);
+ pure_app_frame(&app);
+ for (uint32_t s = 0; s < module_b.states; ++s) CHECK(isfinite(app.values[s]));
+ }
+ pure_app_stop(&app);
+ CHECK(memory.opened == memory.closed);
+ for (size_t i = 0; i < memory.count; ++i) free(memory.docs[i].bytes);
+ }
+ printf("{\"message_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
+static Spec text_spec;
+
+typedef struct { char text[256]; size_t size; } Text;
+
+static bool text_span(void *context, PureSpan span) {
+ Text *t = context;
+ CHECK(t->size + span.size < sizeof(t->text));
+ memcpy(t->text + t->size, span.data, span.size);
+ t->size += span.size;
+ t->text[t->size] = 0;
+ return true;
+}
+
+static const char *view_text(PureApp *app, uint32_t view) {
+ static Text t;
+ t.size = 0;
+ t.text[0] = 0;
+ CHECK(pure_app_view_text(app, view, text_span, &t) == PURE_OK);
+ return t.text;
+}
+
+static const char *state_text(const PureApp *app, uint32_t state) {
+ static Text t;
+ t.size = 0;
+ t.text[0] = 0;
+ CHECK(pure_draft_read(app->texts[state], 0, pure_draft_size(app->texts[state]), text_span, &t) == PURE_OK);
+ return t.text;
+}
+
+static double number_view(PureApp *app, uint32_t view) {
+ double value;
+ CHECK(pure_app_view(app, view, &value));
+ return value;
+}
+
+enum { CUL = PURE_OP_CUR_LOAD, CS = PURE_OP_CUR_STORE, CM = PURE_OP_CUR_MOVE, TI = PURE_OP_TEXT_INPUT,
+ TINS = PURE_OP_TEXT_INSERT, TREM = PURE_OP_TEXT_REMOVE, TL = PURE_OP_TEXT_LOAD, TK = PURE_OP_TEXT_CONST,
+ CHR = PURE_OP_CHARS, NT = PURE_OP_NUM_TEXT, CAT = PURE_OP_CONCAT, TLN = PURE_OP_TEXT_LINE,
+ CC = PURE_OP_CUR_COUNT, CST = PURE_OP_CUR_START };
+
+/* A small text editor module: states edits (num), note (text), at (cursor in
+ * note), title (text). The runtime keeps at on character boundaries and
+ * moves it with edits; a failed event restores text and cursor. */
+static void texts(void) {
+ static const S nums[] = {{"edits", 0}};
+ static const T textual[] = {{"note", 1, 0}, {"at", 2, 1}, {"title", 1, 1}};
+ static const char *const constants[] = {"\xea\xb0\x80" "a\nbc", "\xeb\xa9\x94\xeb\xaa\xa8", "XYZ",
+ " \xc2\xb7 ", "\xec\x9e\x90"};
+ static const I inputs[] = {
+ {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("left", "ArrowLeft"), KEY("right", "ArrowRight"),
+ KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("back", "Backspace"), KEY("home", "Home"), KEY("bad", "KeyB"),
+ };
+ static const O code[] = {
+ {CUL, 2, 0}, {TI, 0, 0}, {TINS, 1, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 0 typed */
+ {CUL, 2, 0}, {C, 0, -1}, {CM, 0, 0}, {CS, 2, 0}, /* 7 left */
+ {CUL, 2, 0}, {C, 0, 1}, {CM, 0, 0}, {CS, 2, 0}, /* 11 right */
+ {CUL, 2, 0}, {C, 0, -1}, {CM, 1, 0}, {CS, 2, 0}, /* 15 up */
+ {CUL, 2, 0}, {C, 0, 1}, {CM, 1, 0}, {CS, 2, 0}, /* 19 down */
+ {CUL, 2, 0}, {C, 0, -1}, {CM, 0, 0}, {CUL, 2, 0}, {TREM, 1, 0}, /* 23 back */
+ {CST, 1, 0}, {CS, 2, 0}, /* 28 home */
+ {CUL, 2, 0}, {TK, 2, 0}, {TINS, 1, 0}, {C, 0, 1}, {C, 0, 0}, {DIV, 0, 0}, {ST, 0, 0}, /* 30 bad */
+ {TL, 1, 0}, {CHR, 0, 0}, /* 37 view chars */
+ {CUL, 2, 0}, {CC, 0, 0}, /* 39 view offset */
+ {CUL, 2, 0}, {CC, 1, 0}, /* 41 view line_no */
+ {CUL, 2, 0}, {TLN, 1, 0}, /* 43 view line */
+ {TL, 3, 0}, {TK, 3, 0}, {CAT, 0, 0}, {TL, 1, 0}, {CHR, 0, 0}, {NT, 0, 0}, {CAT, 0, 0},
+ {TK, 4, 0}, {CAT, 0, 0}, /* 45 view header */
+ {C, 0, 1}, {C, 0, 3}, {DIV, 0, 0}, {NT, 0, 0}, /* 54 view ratio */
+ };
+ static const H handlers[] = {{1, 0, 7}, {3, 7, 4}, {5, 11, 4}, {7, 15, 4}, {9, 19, 4}, {11, 23, 5},
+ {13, 28, 2}, {15, 30, 7}};
+ static const V number_views[] = {{"chars", 37, 2}, {"offset", 39, 2}, {"line_no", 41, 2}};
+ static const V text_views[] = {{"line", 43, 2}, {"header", 45, 9}, {"ratio", 54, 4}};
+ Spec spec = {nums, 1, inputs, LEN(inputs), number_views, 3, handlers, LEN(handlers), code, LEN(code),
+ NULL, 0, textual, 3, constants, LEN(constants), text_views, 3, NULL, 0, NULL, 0};
+ text_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);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80" "a\nbc") && app.values[2] == 0);
+ for (int i = 0; i < 6; ++i) CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.values[2] == 7); /* stops at the end */
+ CHECK(number_view(&app, 0) == 5 && number_view(&app, 2) == 2 && !strcmp(view_text(&app, 3), "bc"));
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* up keeps column 2 */
+ const char *han = "\xed\x95\x9c";
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)han, 3}) == PURE_OK);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80" "a\xed\x95\x9c\nbc") && app.values[2] == 7 && app.values[0] == 1);
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* back over 3 bytes */
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 3);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80\nbc"));
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); /* stops at the start */
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); /* nothing before */
+ CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* down to column 0 */
+ /* A failing event restores the text, the cursor and the states. */
+ CHECK(pure_app_input(&app, 7, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == PURE_FAULT_DIVIDE);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80\nbc") && app.values[2] == 4 && app.values[0] == 1);
+ CHECK(!strcmp(view_text(&app, 4), "\xeb\xa9\x94\xeb\xaa\xa8 \xc2\xb7 4\xec\x9e\x90"));
+ CHECK(!strcmp(view_text(&app, 5), "0.3333333333333333"));
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xff", 1}) == PURE_INVALID);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xed\xa0\x80", 3}) == PURE_INVALID); /* surrogate */
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xf0\x9f\x98\x80", 4}) == PURE_OK);
+ CHECK(app.values[2] == 8 && number_view(&app, 1) == 3); /* offset in characters */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* back over the emoji */
+ CHECK(pure_app_input(&app, 6, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0);
+ /* Replacement keeps the text and its cursor by name. */
+ static const S fresh[] = {{"fresh", 7}};
+ static const T kept[] = {{"note", 1, 0}, {"at", 2, 1}};
+ static const char *const other[] = {"ignored"};
+ Spec next = {fresh, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, kept, 2, other, 1, text_views, 0, NULL, 0, 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);
+ CHECK(app.values[0] == 7 && app.values[2] == 3 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\n\xf0\x9f\x98\x80" "bc"));
+ pure_app_stop(&app);
+
+ /* Verification of text operations. */
+ static const T two[] = {{"one", 1, 0}, {"two", 1, 0}, {"c", 2, 1}};
+ static const O wrong_text[] = {{CUL, 3, 0}, {TK, 0, 0}, {TINS, 2, 0}};
+ static const H frame[] = {{0, 0, 3}};
+ Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, wrong_text, 3, NULL, 0, two, 3, constants, 1, text_views, 0, NULL, 0, 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;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text input outside"));
+ static const O store_text[] = {{TK, 0, 0}, {ST, 0, 0}};
+ static const H frame2[] = {{0, 0, 2}};
+ bad.o = store_text; bad.no = 2; bad.h = frame2;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ static const O load_text[] = {{L, 1, 0}, {ST, 0, 0}};
+ bad.o = load_text;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state"));
+ static const V text_as_number[] = {{"v", 0, 1}};
+ static const O constant_only[] = {{TK, 0, 0}};
+ bad = (Spec){nums, 1, inputs, 0, text_as_number, 1, NULL, 0, constant_only, 1, NULL, 0, two, 3, constants, 1, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view must leave one value of its type"));
+ static const T cursor_on_number[] = {{"c", 2, 0}};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, cursor_on_number, 1, constants, 1, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference"));
+ static const char *const broken[] = {"\xc0\xaf"};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text constant"));
+}
+
+static bool ignore_span(void *context, PureSpan span) {
+ (void)context;
+ CHECK(pure_utf8_valid(span) || span.size);
+ return true;
+}
+
+/* Damaged text modules either fail to load or edit text safely: every cursor
+ * stays within its text and every text stays valid UTF-8. */
+static void text_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, text_spec);
+ static const char *const words[] = {"a", "\xea\xb0\x80", "\n", "\xf0\x9f\x98\x80", "xy\nz", ""};
+ uint32_t state = 0x1b873593u;
+ 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 < 16; ++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_CURSOR) {
+ PureDraft *text = app.texts[module_b.state_ref[s]];
+ size_t length = pure_draft_size(text), at = (size_t)app.values[s];
+ CHECK(app.values[s] >= 0 && at <= length && app.values[s] == (double)at);
+ if (at < length) { /* never inside a multi-byte character */
+ Text t = {{0}, 0};
+ pure_draft_read(text, at, 1, text_span, &t);
+ CHECK(((unsigned char)t.text[0] & 0xC0) != 0x80);
+ }
+ } else if (module_b.state_type[s] == PURE_STATE_TEXT) {
+ unsigned utf8 = 0;
+ bool ok = true;
+ Text t = {{0}, 0};
+ if (pure_draft_size(app.texts[s]) < 200) {
+ pure_draft_read(app.texts[s], 0, pure_draft_size(app.texts[s]), text_span, &t);
+ ok = pure_utf8_step(&utf8, (PureSpan){(const unsigned char *)t.text, t.size}) &&
+ pure_utf8_complete(utf8);
+ }
+ CHECK(ok);
+ } else CHECK(isfinite(app.values[s]));
+ }
+ }
+ pure_app_stop(&app);
+ }
+ 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, NULL, 0};
+ 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(app.fault == PURE_FAULT_NONE); /* views leave faults alone */
+ 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, NULL, 0};
+ CHECK(load(same, &module_b) == PURE_OK);
+ const PureList *before = app.lists[1];
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.lists[1] == before && app.values[2] == 2);
+ 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, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "index of another list"));
+ static const O swapped[] = {{C, 0, 0}, {TK, 0, 0}, {LA, 1, 0}};
+ static const H frame3[] = {{0, 0, 3}};
+ 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, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
+ static const T index_of_number[] = {{"ia", 4, 0}};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, index_of_number, 1, constants, 3, NULL, 0,
+ records, 1, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference"));
+ static const T list_of_nothing[] = {{"a", 3, 1}};
+ bad.t = list_of_nothing;
+ 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);
+}
+
+static Spec table_spec;
+
+static const char *cell_text(PureApp *app, uint32_t column, size_t row) {
+ static Text t;
+ t.size = 0;
+ t.text[0] = 0;
+ CHECK(pure_app_cell_text(app, column, row, text_span, &t) == PURE_OK);
+ return t.text;
+}
+
+enum { ROW = PURE_OP_ROW, EQ = PURE_OP_EQ, LT = PURE_OP_LT, PICK = PURE_OP_PICK, SLIDE = PURE_OP_SLIDE };
+
+/* A table view has a row per item; its columns read the row and may branch.
+ * A value crosses a join when every path brings the same stack. */
+static void tables(void) {
+ static const S nums[] = {{"n", 0}};
+ static const T states[] = {{"tasks", 3, 0}, {"at", 4, 1}};
+ static const RF task_fields[] = {{"title", 1, 0}, {"done", 0, 0}};
+ static const R records[] = {{"task", task_fields, 2}};
+ static const char *const constants[] = {"", "x", " ", "empty"};
+ static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "Space")};
+ static const O code[] = {
+ {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, /* 0 typed */
+ {XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 5 toggle */
+ {LC, 1, 0}, {C, 0, 0}, {GT, 0, 0}, {JZ, 17, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {JMP, 18, 0},
+ {TK, 3, 0}, /* 10 current */
+ {ROW, 1, 0}, {IG, TITLE, 0}, /* 18 title */
+ {ROW, 1, 0}, {IG, DONE, 0}, {JZ, 25, 0}, {TK, 1, 0}, {JMP, 26, 0}, {TK, 2, 0}, /* 20 mark */
+ {ROW, 1, 0}, {XN, 0, 0}, /* 26 pos */
+ {ROW, 1, 0}, {XN, 0, 0}, {XL, 2, 0}, {XN, 0, 0}, {EQ, 0, 0}, /* 28 here */
+ };
+ static const H handlers[] = {{1, 0, 5}, {3, 5, 5}};
+ static const V text_views[] = {{"current", 10, 8}};
+ static const W table[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"mark", 1, 2, 20, 6},
+ {"pos", 0, 2, 26, 2}, {"here", 0, 2, 28, 5}};
+ Spec spec = {nums, 1, inputs, 2, NULL, 0, handlers, 2, code, LEN(code), NULL, 0, states, 2, constants, 4,
+ text_views, 1, records, 1, table, 5};
+ table_spec = spec;
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(pure_module_view(&module_a, "board") == 1 && pure_module_view(&module_a, "title") == -1 &&
+ pure_module_column(&module_a, 1, "title") == 2 && pure_module_column(&module_a, 1, "current") == -1);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ double value;
+ CHECK(pure_app_rows(&app, 1) == 0 && !strcmp(view_text(&app, 0), "empty")); /* the else branch */
+ CHECK(pure_app_cell_text(&app, 2, 0, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"a", 1}) == PURE_OK);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xeb\xb9\xb5", 3}) == PURE_OK);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(pure_app_rows(&app, 1) == 2 && !strcmp(view_text(&app, 0), "\xeb\xb9\xb5"));
+ CHECK(!strcmp(cell_text(&app, 2, 0), "a") && !strcmp(cell_text(&app, 2, 1), "\xeb\xb9\xb5"));
+ CHECK(!strcmp(cell_text(&app, 3, 0), " ") && !strcmp(cell_text(&app, 3, 1), "x"));
+ CHECK(pure_app_cell(&app, 4, 1, &value) && value == 1 && pure_app_cell(&app, 5, 0, &value) && value == 0 &&
+ pure_app_cell(&app, 5, 1, &value) && value == 1);
+ CHECK(!pure_app_cell(&app, 4, 2, &value) && pure_app_cell_text(&app, 2, 2, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND);
+ /* Columns are read by row, other views without one. */
+ CHECK(!pure_app_view(&app, 4, &value) && pure_app_view_text(&app, 2, text_span, &(Text){{0}, 0}) == PURE_INVALID);
+ CHECK(pure_app_cell_text(&app, 0, 0, text_span, &(Text){{0}, 0}) == PURE_INVALID && pure_app_rows(&app, 0) == 0);
+ CHECK(pure_app_view_text(&app, 1, text_span, &(Text){{0}, 0}) == PURE_INVALID);
+ pure_app_stop(&app);
+
+ /* Verification of rows, tables and joins. */
+ Spec bad = spec;
+ static const V row_view[] = {{"current", 18, 2}};
+ bad.tv = row_view;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "row outside a column"));
+ static const H row_handler[] = {{3, 26, 2}};
+ bad = spec; bad.h = row_handler; bad.nh = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "row outside a column"));
+ static const W twice[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"title", 1, 2, 18, 2}};
+ bad = spec; bad.w = twice; bad.nw = 3;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate column"));
+ static const W two_tables[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"other", 2, 0, 1, 0},
+ {"title", 1, 4, 18, 2}, {"tasks", 0, 4, 26, 2}};
+ bad = spec; bad.w = two_tables; bad.nw = 5;
+ CHECK(load(bad, &module_b) == PURE_OK); /* column names belong to their table */
+ static const W not_a_table[] = {{"title", 1, 1, 18, 2}};
+ bad = spec; bad.w = not_a_table; bad.nw = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view type"));
+ static const W over_number[] = {{"board", 2, 0, 0, 0}};
+ bad = spec; bad.w = over_number; bad.nw = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "table list"));
+ static const O other_row[] = {{ROW, 2, 0}, {XN, 0, 0}};
+ static const W other_row_column[] = {{"board", 2, 0, 1, 0}, {"pos", 0, 1, 0, 2}};
+ bad = spec; bad.o = other_row; bad.no = 2; bad.h = NULL; bad.nh = 0; bad.tv = NULL; bad.ntv = 0;
+ bad.w = other_row_column; bad.nw = 2;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state"));
+ static const O typed_join[] = {{L, 0, 0}, {JZ, 4, 0}, {TK, 0, 0}, {JMP, 5, 0}, {C, 0, 1}};
+ static const V mixed[] = {{"current", 0, 5}};
+ bad = spec; bad.o = typed_join; bad.no = 5; bad.h = NULL; bad.nh = 0; bad.tv = mixed; bad.w = NULL; bad.nw = 0;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks"));
+ /* At most 8 joins with a value may be ahead at once. */
+ for (unsigned open = 8; open <= 9; ++open) {
+ static O nested[1 + 2 * 9 + 9 + 1];
+ unsigned n = 0;
+ nested[n++] = (O){C, 0, 1};
+ for (unsigned k = 0; k < open; ++k) { nested[n++] = (O){C, 0, 0}; nested[n++] = (O){JZ, 1 + 2 * open + k, 0}; }
+ for (unsigned k = 0; k < open; ++k) nested[n++] = (O){NEG, 0, 0};
+ nested[n++] = (O){ST, 0, 0};
+ H frame = {0, 0, n};
+ bad = spec; bad.o = nested; bad.no = n; bad.h = &frame; bad.nh = 1; bad.tv = NULL; bad.ntv = 0; bad.w = NULL; bad.nw = 0;
+ CHECK(load(bad, &module_b) == (open == 8 ? PURE_OK : PURE_INVALID));
+ }
+ CHECK(strstr(error, "more than 8 joins ahead"));
+
+ /* PICK copies a value from below, SLIDE drops values under the top: a
+ * value used twice is computed once. (3 + 4) * (3 + 4) with a text between. */
+ static const O picked[] = {{C, 0, 3}, {C, 0, 4}, {ADD, 0, 0}, {TK, 1, 0}, {PICK, 1, 0}, {PICK, 2, 0}, {MUL, 0, 0},
+ {SLIDE, 2, 0}};
+ static const V squared[] = {{"sq", 0, 8}};
+ bad = (Spec){nums, 1, inputs, 0, squared, 1, NULL, 0, picked, 8, NULL, 0, NULL, 0, constants, 2, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_b, (PureData){0}) == PURE_OK && number_view(&app, 0) == 49);
+ pure_app_stop(&app);
+ static const O pick_text[] = {{TK, 1, 0}, {C, 0, 1}, {PICK, 1, 0}, {SLIDE, 2, 0}};
+ static const V text_left[] = {{"t", 0, 4}};
+ bad.v = NULL; bad.nv = 0; bad.tv = text_left; bad.ntv = 1; bad.o = pick_text; bad.no = 4;
+ CHECK(load(bad, &module_b) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_b, (PureData){0}) == PURE_OK && !strcmp(view_text(&app, 0), "x"));
+ pure_app_stop(&app);
+ static const O pick_type[] = {{TK, 1, 0}, {PICK, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {SLIDE, 1, 0}};
+ static const V five[] = {{"sq", 0, 5}}, three[] = {{"sq", 0, 3}};
+ bad.o = pick_type; bad.no = 5; bad.tv = NULL; bad.ntv = 0; bad.v = five; bad.nv = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type")); /* a copy keeps its type */
+ static const O pick_far[] = {{C, 0, 1}, {PICK, 1, 0}, {SLIDE, 1, 0}};
+ bad.o = pick_far; bad.no = 3; bad.v = three;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "stack underflow"));
+ static const O slide_all[] = {{C, 0, 1}, {C, 0, 2}, {SLIDE, 2, 0}};
+ bad.o = slide_all;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "stack underflow"));
+ static const O slide_none[] = {{C, 0, 1}, {C, 0, 2}, {SLIDE, 0, 0}};
+ bad.o = slide_none;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "stack underflow"));
+}
+
+/* Damaged table modules either fail to load or evaluate every cell safely. */
+static void table_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, table_spec);
+ uint32_t state = 0xc2b2ae35u;
+ unsigned accepted = 0;
+ for (unsigned round = 0; round < 20000; ++round) {
+ memcpy(bytes, valid, size);
+ unsigned flips = 1 + next_random(&state) % 3;
+ for (unsigned f = 0; f < flips; ++f)
+ bytes[next_random(&state) % size] ^= (unsigned char)(1u << (next_random(&state) % 8));
+ if (pure_module_load(bytes, size, &module_b, error, sizeof(error)) != PURE_OK) continue;
+ ++accepted;
+ PureApp app;
+ if (pure_app_start(&app, &module_b, (PureData){0}) != PURE_OK) continue;
+ for (unsigned e = 0; e < 12; ++e) {
+ uint32_t input = next_random(&state) % 2;
+ if (module_b.input[input].kind == PURE_KIND_TEXT) pure_app_text(&app, input, (PureSpan){(const unsigned char *)"t", 1});
+ else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN);
+ for (uint32_t v = 0; v < module_b.views; ++v) {
+ double value;
+ if (!module_b.view_table[v]) {
+ if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_view_text(&app, v, ignore_span, NULL);
+ else pure_app_view(&app, v, &value);
+ continue;
+ }
+ size_t rows = pure_app_rows(&app, module_b.view_table[v] - 1u);
+ for (size_t row = 0; row <= rows; ++row) {
+ if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_cell_text(&app, v, row, ignore_span, NULL);
+ else if (pure_app_cell(&app, v, row, &value)) CHECK(isfinite(value));
+ }
+ }
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"table_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
+/* ---- Stored states ---------------------------------------------------- */
+
+static char *new_store(void) {
+ static char path[64];
+ snprintf(path, sizeof(path), "/tmp/pure-module-store-XXXXXX");
+ CHECK(mkdtemp(path) && pure_store_init(path) == PURE_OK);
+ return path;
+}
+
+static void remove_store(const char *path) {
+ DIR *entries = opendir(path);
+ CHECK(entries);
+ struct dirent *entry;
+ while ((entry = readdir(entries)))
+ if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) CHECK(unlinkat(dirfd(entries), entry->d_name, 0) == 0);
+ CHECK(closedir(entries) == 0 && rmdir(path) == 0);
+}
+
+typedef struct { unsigned char bytes[4096]; size_t size; } Bytes;
+
+static bool bytes_span(void *context, PureSpan span) {
+ Bytes *b = context;
+ CHECK(b->size + span.size <= sizeof(b->bytes));
+ memcpy(b->bytes + b->size, span.data, span.size);
+ b->size += span.size;
+ return true;
+}
+
+/* Two apps of one module hold equal states: numbers bit for bit, texts and
+ * list items byte for byte. */
+static void same_states(const PureApp *a, const PureApp *b) {
+ const PureModule *m = a->module;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (m->state_type[i] == PURE_STATE_TEXT) {
+ static Bytes x, y;
+ x.size = y.size = 0;
+ CHECK(pure_draft_read(a->texts[i], 0, pure_draft_size(a->texts[i]), bytes_span, &x) == PURE_OK);
+ CHECK(pure_draft_read(b->texts[i], 0, pure_draft_size(b->texts[i]), bytes_span, &y) == PURE_OK);
+ CHECK(x.size == y.size && !memcmp(x.bytes, y.bytes, x.size));
+ } else if (m->state_type[i] == PURE_STATE_LIST) {
+ const PureRecord *r = &m->record[m->state_ref[i]];
+ CHECK(pure_list_count(a->lists[i]) == pure_list_count(b->lists[i]));
+ for (size_t at = 0; at < pure_list_count(a->lists[i]); ++at)
+ for (unsigned f = 0; f < r->count; ++f) {
+ PureCell x = pure_list_get(a->lists[i], at, f), y = pure_list_get(b->lists[i], at, f);
+ CHECK(!memcmp(&x.num, &y.num, sizeof(x.num)) && x.size == y.size && (!x.size || !memcmp(x.text, y.text, x.size)));
+ }
+ } else CHECK(!memcmp(&a->values[i], &b->values[i], sizeof(double)));
+ }
+}
+
+/* The same random events on an app in memory and on one whose states are
+ * stored and which restarts from them every few events: equal after every
+ * step, failed events included. */
+static void stored_equivalence(Spec spec, const char *const *words, size_t word_count, uint32_t seed) {
+ char *path = new_store();
+ CHECK(load(spec, &module_a) == PURE_OK);
+ PureApp memory, stored;
+ CHECK(pure_app_start(&memory, &module_a, (PureData){0}) == PURE_OK);
+ CHECK(pure_app_start(&stored, &module_a, pure_tree_data(path)) == PURE_OK);
+ unsigned restarts = 0;
+ for (unsigned step = 0; step < 400; ++step) {
+ uint32_t input = next_random(&seed) % module_a.inputs;
+ PureResult a, b;
+ if (module_a.input[input].kind == PURE_KIND_TEXT) {
+ const char *word = words[next_random(&seed) % word_count];
+ PureSpan text = {(const unsigned char *)word, strlen(word)};
+ a = pure_app_text(&memory, input, text);
+ b = pure_app_text(&stored, input, text);
+ } else {
+ a = pure_app_input(&memory, input, PURE_EDGE_DOWN);
+ b = pure_app_input(&stored, input, PURE_EDGE_DOWN);
+ }
+ CHECK(a == b);
+ same_states(&memory, &stored);
+ if (next_random(&seed) % 7 == 0) {
+ pure_app_stop(&stored);
+ CHECK(pure_app_start(&stored, &module_a, pure_tree_data(path)) == PURE_OK);
+ same_states(&memory, &stored);
+ ++restarts;
+ }
+ }
+ CHECK(restarts > 20);
+ pure_app_stop(&memory);
+ pure_app_stop(&stored);
+ remove_store(path);
+}
+
+/* One event changes a number, a text and a list; the process dies at each
+ * commit stage. Starting again finds all three as before or all as after. */
+enum { NOTE = 1, TASKS = 3 };
+static const S crash_nums[] = {{"n", 0}};
+static const T crash_states[] = {{"note", 1, 0}, {"at", 2, 1}, {"tasks", 3, 0}, {"i", 4, 3}};
+static const RF crash_fields[] = {{"title", 1, 0}, {"done", 0, 0}};
+static const R crash_records[] = {{"task", crash_fields, 2}};
+static const char *const crash_constants[] = {"", "x"};
+static const I crash_inputs[] = {KEY("go", "KeyG")};
+static const O crash_code[] = {
+ {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* n = n + 1 */
+ {CUL, 2, 0}, {TK, 1, 0}, {TINS, 1, 0}, /* note.insert(at, "x") */
+ {TK, 1, 0}, {L, 0, 0}, {LA, 3, 0}, /* tasks.append(task { "x", n }) */
+ {XE, 3, 0}, {XS, 4, 0}, /* i = tasks.last */
+};
+static const H crash_handlers[] = {{1, 0, 12}};
+
+static unsigned consistent(const PureApp *app) {
+ unsigned n = (unsigned)app->values[0];
+ CHECK(app->values[0] == n && pure_draft_size(app->texts[NOTE]) == n && app->values[2] == n);
+ CHECK(pure_list_count(app->lists[TASKS]) == n && app->values[4] == (n ? n - 1 : 0));
+ for (unsigned k = 0; k < n; ++k) CHECK(pure_list_get(app->lists[TASKS], k, 1).num == k + 1);
+ return n;
+}
+
+static void stored_crashes(void) {
+ char *path = new_store();
+ Spec spec = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0,
+ crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0};
+ CHECK(load(spec, &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && consistent(&app) == 1);
+ pure_app_stop(&app);
+ unsigned expected = 1;
+ for (int stage = PURE_AFTER_HEADER; stage <= PURE_BEFORE_DIR_SYNC; ++stage) {
+ pid_t child = fork();
+ CHECK(child >= 0);
+ if (!child) {
+ PureApp doomed;
+ if (pure_app_start(&doomed, &module_a, pure_tree_data(path)) != PURE_OK) _exit(1);
+ crash = 1;
+ fail_stage = stage;
+ pure_app_input(&doomed, 0, PURE_EDGE_DOWN);
+ _exit(0);
+ }
+ int status;
+ CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77);
+ if (stage == PURE_BEFORE_DIR_SYNC) ++expected; /* the head was written */
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == expected);
+ pure_app_stop(&app);
+ }
+ /* A failed commit leaves the app and the store as they were; after an
+ * uncertain one the app no longer matches the store and its events fail
+ * until it starts again, from what the store has. */
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ for (fail_stage = PURE_AFTER_HEADER; fail_stage <= PURE_BEFORE_RENAME; ++fail_stage) {
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_STORE_ERROR && consistent(&app) == expected);
+ }
+ fail_stage = PURE_BEFORE_DIR_SYNC;
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_UNCERTAIN && consistent(&app) == expected);
+ fail_stage = -1;
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_CONFLICT && consistent(&app) == expected);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == expected + 1);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && consistent(&app) == expected + 2);
+ pure_app_stop(&app);
+ remove_store(path);
+}
+
+/* Frame changes are stored by the next event or by a save; a stop alone
+ * leaves them out. */
+static void stored_frames(void) {
+ char *path = new_store();
+ static const O code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}};
+ static const H handlers[] = {{0, 0, 4}, {1, 0, 0}};
+ Spec spec = {crash_nums, 1, crash_inputs, 1, NULL, 0, handlers, 2, code, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0,
+ NULL, 0, NULL, 0};
+ CHECK(load(spec, &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ for (int i = 0; i < 5; ++i) CHECK(pure_app_frame(&app) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 0);
+ for (int i = 0; i < 3; ++i) CHECK(pure_app_frame(&app) == PURE_OK);
+ CHECK(pure_app_save(&app) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 3);
+ for (int i = 0; i < 2; ++i) CHECK(pure_app_frame(&app) == PURE_OK);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 5);
+ pure_app_stop(&app);
+ remove_store(path);
+}
+
+static bool list_keys(void *context, const char *key) {
+ strcat(context, key);
+ strcat(context, ",");
+ return true;
+}
+
+/* A message's document and the states its handlers change commit together. */
+static void stored_messages(void) {
+ char *path = new_store();
+ CHECK(load(editor_spec, &module_a) == PURE_OK);
+ PureApp app;
+ unsigned char buffer[64];
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "hello")) == PURE_OK && app.revision == 1);
+ CHECK(app.values[0] == 1);
+ CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "x")) == PURE_CONFLICT && app.values[0] == 1);
+ /* An edit past the size guard fails: neither the document nor the count moves. */
+ CHECK(pure_app_message(&app, 3, "doc", 1, edit_payload(buffer, 5, 0, "-world")) == PURE_LIMIT && app.values[0] == 1);
+ CHECK(pure_app_message(&app, 3, "doc", 1, edit_payload(buffer, 5, 0, "!")) == PURE_OK && app.values[0] == 2);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 2);
+ CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && app.revision == 2 && replied(&app, "hello!"));
+ pure_app_stop(&app);
+ /* Documents stay documents: the app's states are not among the keys. */
+ char seen[256] = "";
+ CHECK(pure_store_keys(path, list_keys, seen) == PURE_OK && !strcmp(seen, "doc,"));
+ remove_store(path);
+}
+
+/* A stored list whose record changed keeps each field by name, when the app
+ * starts from it and when its module is replaced; stored values that no
+ * longer fit fall back, and damaged stored values stop the start. */
+static void stored_migration(void) {
+ char *path = new_store();
+ static const RF changed_fields[] = {{"done", 0, 0}, {"note", 1, 1}, {"title", 1, 0}, {"rank", 0, 5}};
+ static const R changed[] = {{"task", changed_fields, 4}};
+ Spec before = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0,
+ crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0};
+ Spec after = before;
+ after.r = changed;
+ after.h = NULL;
+ after.nh = 0;
+ CHECK(load(before, &module_a) == PURE_OK && load(after, &module_b) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ pure_app_stop(&app);
+ for (int round = 0; round < 2; ++round) { /* migrated, then stored as migrated */
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && pure_list_count(app.lists[TASKS]) == 2);
+ PureCell note = pure_list_get(app.lists[TASKS], 1, 1), title = pure_list_get(app.lists[TASKS], 1, 2);
+ CHECK(pure_list_get(app.lists[TASKS], 1, 0).num == 2 && pure_list_get(app.lists[TASKS], 1, 3).num == 5);
+ CHECK(note.size == 1 && note.text[0] == 'x' && title.size == 1 && title.text[0] == 'x');
+ CHECK(app.values[0] == 2 && app.values[4] == 1 && pure_draft_size(app.texts[NOTE]) == 2);
+ pure_app_stop(&app);
+ }
+ /* Back to the first record by replacing the module; the next save stores it. */
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_a) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == 3);
+ pure_app_stop(&app);
+ /* Stored numbers that no longer fit their state start again. */
+ PureDraft *values;
+ int64_t revision;
+ CHECK(pure_state_open(path, "values", &values) == PURE_OK);
+ unsigned char record[64] = "PVS1";
+ size_t at = 4;
+ const char *names[] = {"at", "i", "n"};
+ double stored[] = {100, 7.5, 9};
+ uint8_t types[] = {PURE_STATE_CURSOR, PURE_STATE_INDEX, PURE_STATE_NUM};
+ for (unsigned k = 0; k < 3; ++k) {
+ record[at++] = types[k];
+ record[at++] = (unsigned char)strlen(names[k]);
+ memcpy(record + at, names[k], strlen(names[k]));
+ at += strlen(names[k]);
+ uint64_t bits;
+ memcpy(&bits, &stored[k], sizeof(bits));
+ for (unsigned b = 0; b < 8; ++b) record[at++] = (unsigned char)(bits >> (8 * b));
+ }
+ CHECK(pure_draft_splice(values, 0, pure_draft_size(values), (PureSpan){record, at}) == PURE_OK);
+ CHECK(pure_draft_commit(values, &revision) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(app.values[0] == 9 && app.values[2] == 0 && app.values[4] == 0);
+ pure_app_stop(&app);
+ /* Damaged stored lists and texts are refused, not replaced. */
+ const char *damaged[] = {"list-tasks", "text-note"};
+ for (unsigned k = 0; k < 2; ++k) {
+ PureDraft *draft;
+ CHECK(pure_state_open(path, damaged[k], &draft) == PURE_OK);
+ CHECK(pure_draft_splice(draft, 0, 0, (PureSpan){(const unsigned char *)"\xff", 1}) == PURE_OK);
+ CHECK(pure_draft_commit(draft, &revision) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_STORE_ERROR);
+ CHECK(pure_draft_splice(draft, 0, 1, (PureSpan){0}) == PURE_OK && pure_draft_commit(draft, &revision) == PURE_OK);
+ pure_draft_close(draft);
+ }
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_list_count(app.lists[TASKS]) == 3 && pure_draft_size(app.texts[NOTE]) == 3 && app.values[0] == 9);
+ pure_app_stop(&app);
+ pure_draft_close(values);
+ remove_store(path);
+}
+
+static void stored_states(void) {
+ static const char *const list_words[] = {"a", "\xea\xb0\x80", "", "\n", "\xf0\x9f\x98\x80"};
+ static const char *const text_words[] = {"a", "\xea\xb0\x80", "\n", "\xf0\x9f\x98\x80", "xy\nz", ""};
+ stored_equivalence(list_spec, list_words, LEN(list_words), 0x9e3779b9u);
+ stored_equivalence(text_spec, text_words, LEN(text_words), 0x7f4a7c15u);
+ stored_crashes();
+ stored_frames();
+ stored_messages();
+ stored_migration();
+}
+
+/* ---- Deferred saving ---------------------------------------------------- */
+
+typedef struct { uint32_t input; const char *word; } Step;
+
+static PureResult apply_step(PureApp *app, Step step) {
+ if (step.word) return pure_app_text(app, step.input, (PureSpan){(const unsigned char *)step.word, strlen(step.word)});
+ return pure_app_input(app, step.input, PURE_EDGE_DOWN);
+}
+
+/* Deferred events apply at once and last only through a save: an app that
+ * stops without saving comes back as it was at the last save. The oracle is
+ * an app in memory that replays exactly the saved events. */
+static void deferred_equivalence(Spec spec, const char *const *words, size_t word_count, uint32_t seed) {
+ char *path = new_store();
+ CHECK(load(spec, &module_a) == PURE_OK);
+ static Step history[2000];
+ size_t saved = 0, applied = 0;
+ PureApp memory, stored;
+ CHECK(pure_app_start(&memory, &module_a, (PureData){0}) == PURE_OK);
+ CHECK(pure_app_start(&stored, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_defer(&stored, true) && !pure_app_defer(&memory, true));
+ unsigned lost = 0, saves = 0;
+ for (unsigned step = 0; step < 600; ++step) {
+ Step next = {next_random(&seed) % module_a.inputs, NULL};
+ if (module_a.input[next.input].kind == PURE_KIND_TEXT) next.word = words[next_random(&seed) % word_count];
+ PureResult a = apply_step(&memory, next), b = apply_step(&stored, next);
+ CHECK(a == b);
+ if (a == PURE_OK) history[applied++] = next;
+ same_states(&memory, &stored);
+ uint32_t roll = next_random(&seed) % 16;
+ if (roll < 3) {
+ CHECK(pure_app_save(&stored) == PURE_OK);
+ saved = applied;
+ ++saves;
+ } else if (roll == 3) {
+ /* Stop without saving: only saved events come back. */
+ pure_app_stop(&stored);
+ pure_app_stop(&memory);
+ CHECK(pure_app_start(&stored, &module_a, pure_tree_data(path)) == PURE_OK && pure_app_defer(&stored, true));
+ CHECK(pure_app_start(&memory, &module_a, (PureData){0}) == PURE_OK);
+ for (size_t k = 0; k < saved; ++k) CHECK(apply_step(&memory, history[k]) == PURE_OK);
+ lost += (unsigned)(applied - saved);
+ applied = saved;
+ same_states(&memory, &stored);
+ }
+ }
+ CHECK(saves > 50 && lost > 0);
+ pure_app_stop(&memory);
+ pure_app_stop(&stored);
+ remove_store(path);
+}
+
+/* A burst of events is saved with one commit: when that commit fails every
+ * event of the burst is undone, and a process that dies while saving comes
+ * back with all of the burst or none of it. */
+static void deferred_bursts(void) {
+ char *path = new_store();
+ Spec spec = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0,
+ crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0};
+ CHECK(load(spec, &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pure_app_defer(&app, true));
+ for (int i = 0; i < 5; ++i) CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(consistent(&app) == 5 && app.events == 5);
+ for (fail_stage = PURE_AFTER_HEADER; fail_stage <= PURE_BEFORE_RENAME; fail_stage += 2) {
+ CHECK(pure_app_save(&app) == PURE_STORE_ERROR && consistent(&app) == 0 && app.events == 0);
+ for (int i = 0; i < 5; ++i) CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ }
+ fail_stage = -1;
+ CHECK(pure_app_save(&app) == PURE_OK && consistent(&app) == 5);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == 5);
+ pure_app_stop(&app);
+ unsigned expected = 5;
+ for (int stage = PURE_AFTER_HEADER; stage <= PURE_BEFORE_DIR_SYNC; ++stage) {
+ pid_t child = fork();
+ CHECK(child >= 0);
+ if (!child) {
+ PureApp doomed;
+ if (pure_app_start(&doomed, &module_a, pure_tree_data(path)) != PURE_OK || !pure_app_defer(&doomed, true)) _exit(1);
+ for (int i = 0; i < 7; ++i) if (pure_app_input(&doomed, 0, PURE_EDGE_DOWN) != PURE_OK) _exit(2);
+ crash = 1;
+ fail_stage = stage;
+ pure_app_save(&doomed);
+ _exit(0);
+ }
+ int status;
+ CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77);
+ if (stage == PURE_BEFORE_DIR_SYNC) expected += 7;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == expected);
+ pure_app_stop(&app);
+ }
+ remove_store(path);
+}
+
+/* Messages in a burst: documents of different keys commit together with the
+ * states, each moving one revision; a second message for a key saves the
+ * burst first, and so does a full burst. */
+static void deferred_messages(void) {
+ char *path = new_store();
+ CHECK(load(editor_spec, &module_a) == PURE_OK);
+ PureApp app;
+ unsigned char buffer[64];
+ char key[8];
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pure_app_defer(&app, true));
+ for (int i = 0; i < 3; ++i) {
+ snprintf(key, sizeof(key), "k%d", i);
+ CHECK(pure_app_message(&app, 1, key, 0, edit_payload(buffer, 0, 0, "ab")) == PURE_OK && app.revision == 1);
+ }
+ CHECK(app.values[0] == 3 && app.pending_count == 3 && app.open);
+ /* The same key again: the burst is saved first, then this edit joins a new one. */
+ CHECK(pure_app_message(&app, 1, "k0", 1, edit_payload(buffer, 2, 0, "c")) == PURE_OK && app.revision == 2);
+ CHECK(app.pending_count == 1 && app.values[0] == 4);
+ CHECK(pure_app_message(&app, 1, "k1", 0, edit_payload(buffer, 0, 0, "x")) == PURE_CONFLICT);
+ CHECK(pure_app_save(&app) == PURE_OK && !app.open && !app.pending_count);
+ for (int i = 0; i < PURE_APP_PENDING + 1; ++i) {
+ snprintf(key, sizeof(key), "m%d", i);
+ CHECK(pure_app_message(&app, 1, key, 0, edit_payload(buffer, 0, 0, "y")) == PURE_OK);
+ CHECK(app.pending_count == (i < PURE_APP_PENDING ? (unsigned)i + 1 : 1)); /* full: saved on the way */
+ }
+ pure_app_stop(&app); /* the last edit was never saved */
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 4 + PURE_APP_PENDING);
+ CHECK(pure_app_message(&app, 0, "k0", -1, (PureSpan){0}) == PURE_OK && app.revision == 2 && replied(&app, "abc"));
+ snprintf(key, sizeof(key), "m%d", PURE_APP_PENDING);
+ CHECK(pure_app_message(&app, 0, key, -1, (PureSpan){0}) == PURE_NOT_FOUND);
+ snprintf(key, sizeof(key), "m%d", PURE_APP_PENDING - 1);
+ CHECK(pure_app_message(&app, 0, key, -1, (PureSpan){0}) == PURE_OK && replied(&app, "y"));
+ pure_app_stop(&app);
+ remove_store(path);
+}
+
+static void deferred(void) {
+ static const char *const list_words[] = {"a", "\xea\xb0\x80", "", "\n", "\xf0\x9f\x98\x80"};
+ static const char *const text_words[] = {"a", "\xea\xb0\x80", "\n", "\xf0\x9f\x98\x80", "xy\nz", ""};
+ deferred_equivalence(list_spec, list_words, LEN(list_words), 0x2545f491u);
+ deferred_equivalence(text_spec, text_words, LEN(text_words), 0x1b873593u);
+ deferred_bursts();
+ deferred_messages();
+}
+
+/* Text inserted at a cursor goes before it; at a mark (a cursor that stays),
+ * after it. Everywhere else, and when text is removed, both move alike. */
+static void marks(void) {
+ enum { MARK = 2 | PURE_CURSOR_STAYS << 8 };
+ static const T textual[] = {{"note", 1, 0}, {"at", 2, 0}, {"mark", MARK, 0}};
+ static const char *const constants[] = {"ab"};
+ static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("keep", "KeyK"), KEY("back", "Backspace")};
+ static const O code[] = {
+ {CUL, 1, 0}, {TI, 0, 0}, {TINS, 0, 0}, /* 0 typed: at the cursor */
+ {CUL, 1, 0}, {CS, 2, 0}, /* 3 keep: mark = at */
+ {CUL, 1, 0}, {C, 0, -1}, {CM, 0, 0}, {CUL, 1, 0}, {TREM, 0, 0}, /* 5 back */
+ };
+ static const H handlers[] = {{1, 0, 3}, {3, 3, 2}, {5, 5, 5}};
+ Spec spec = {NULL, 0, inputs, LEN(inputs), NULL, 0, handlers, LEN(handlers), code, LEN(code),
+ NULL, 0, textual, 3, constants, 1, NULL, 0, NULL, 0, NULL, 0};
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(module_a.state_flags[2] == PURE_CURSOR_STAYS && !module_a.state_flags[1]);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"x", 1}) == PURE_OK);
+ CHECK(!strcmp(state_text(&app, 0), "xab") && app.values[1] == 1 && app.values[2] == 0); /* the mark stayed */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 1);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"yz", 2}) == PURE_OK);
+ CHECK(!strcmp(state_text(&app, 0), "xyzab") && app.values[1] == 3 && app.values[2] == 1);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 2 && app.values[2] == 1);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 1 && app.values[2] == 1); /* gathered */
+ pure_app_stop(&app);
+ /* A flag only on a cursor, and only this one. */
+ static const T wrong_flag[] = {{"note", 1, 0}, {"at", 2 | 2 << 8, 0}, {"mark", MARK, 0}};
+ static const T flagged_text[] = {{"note", 1 | PURE_CURSOR_STAYS << 8, 0}, {"at", 2, 0}, {"mark", 2, 0}};
+ Spec bad = spec;
+ bad.t = wrong_flag;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference"));
+ bad.t = flagged_text;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference"));
+}
+
+/* Folds: a block run once per item of a list, as a column of it, gives a
+ * count, a sum, a maximum or a minimum; an empty maximum has no value. */
+enum { FOLD = PURE_OP_FOLD };
+#define FOLDING(kind, list, length) ((uint32_t)(kind) | (uint32_t)(list) << 8 | (uint32_t)(length) << 16)
+static const RF fold_fields[] = {{"title", 1, 0}, {"done", 0, 0}, {"points", 0, 0}};
+static const R fold_records[] = {{"task", fold_fields, 3}};
+static const T fold_states[] = {{"tasks", 3, 0}, {"at", 4, 0}};
+static const char *const fold_constants[] = {""};
+static const I fold_inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "KeyT")};
+enum { F_TITLE = 0, F_DONE = 1u << 16, F_POINTS = 2u << 16 };
+static const O fold_code[] = {
+ {TI, 0, 0}, {C, 0, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */
+ {XL, 1, 0}, {XL, 1, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, {IS, F_DONE, 0}, /* 7 toggle */
+ {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 3), 0}, {ROW, 0, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, /* 12 open */
+ {FOLD, FOLDING(PURE_FOLD_SUM, 0, 3), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {C, 0, 1}, /* 16 points */
+ {FOLD, FOLDING(PURE_FOLD_MAX, 0, 4), 0}, {ROW, 0, 0}, {IG, F_TITLE, 0}, {CHR, 0, 0}, /* 20 longest */
+ {C, 0, 1},
+ {FOLD, FOLDING(PURE_FOLD_SUM, 0, 4), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {ROW, 0, 0}, /* 25 done_points */
+ {IG, F_DONE, 0},
+ {FOLD, FOLDING(PURE_FOLD_MIN, 0, 5), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {ROW, 0, 0}, /* 30 fewest_open */
+ {IG, F_DONE, 0}, {NOT, 0, 0},
+ {C, 0, 10}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}, {ADD, 0, 0}, /* 36 count_plus */
+ {FOLD, FOLDING(PURE_FOLD_FIRST, 0, 3), 0}, {ROW, 0, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, /* 40 first_open */
+ {XN, 0, 0},
+};
+static const H fold_handlers[] = {{1, 0, 7}, {3, 7, 5}};
+static const V fold_views[] = {{"open", 12, 4}, {"points", 16, 4}, {"longest", 20, 5}, {"done_points", 25, 5},
+ {"fewest_open", 30, 6}, {"count_plus", 36, 4}, {"first_open", 40, 5}};
+static Spec fold_spec(const O *code, size_t count, const V *views, size_t nviews, const H *handlers, size_t nhandlers) {
+ return (Spec){NULL, 0, fold_inputs, LEN(fold_inputs), views, nviews, handlers, nhandlers, code, count, NULL, 0,
+ fold_states, 2, fold_constants, 1, NULL, 0, fold_records, 1, NULL, 0};
+}
+
+static void folds(void) {
+ CHECK(load(fold_spec(fold_code, LEN(fold_code), fold_views, LEN(fold_views), fold_handlers, 2), &module_a) == PURE_OK);
+ PureApp app;
+ double value;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ CHECK(number_view(&app, 0) == 0 && number_view(&app, 1) == 0 && !pure_app_view(&app, 2, &value));
+ CHECK(number_view(&app, 3) == 0 && !pure_app_view(&app, 4, &value) && number_view(&app, 5) == 10);
+ CHECK(!pure_app_view(&app, 6, &value)); /* no first open task yet */
+ const char *const words[] = {"a", "bbb", "\xea\xb0\x80\xea\xb0\x80"}; /* one, three and two characters */
+ for (size_t i = 0; i < 3; ++i)
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)words[i], strlen(words[i])}) == PURE_OK);
+ CHECK(number_view(&app, 0) == 3 && number_view(&app, 1) == 6 && number_view(&app, 2) == 3);
+ CHECK(number_view(&app, 3) == 0 && number_view(&app, 4) == 1 && number_view(&app, 5) == 13);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); /* the last task is done */
+ CHECK(number_view(&app, 0) == 2 && number_view(&app, 3) == 2 && number_view(&app, 4) == 1);
+ CHECK(number_view(&app, 6) == 0); /* the first open one: an index */
+ CHECK(pure_app_point(&app, 1, 0) == PURE_OK && pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(number_view(&app, 6) == 1);
+ CHECK(pure_app_point(&app, 1, 1) == PURE_OK && pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(!pure_app_view(&app, 6, &value));
+ CHECK(number_view(&app, 0) == 0 && number_view(&app, 3) == 6 && !pure_app_view(&app, 4, &value)); /* none open */
+ pure_app_stop(&app);
+
+ /* What a fold may not be. */
+ static const O nested[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 2), 0}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
+ static const O into[] = {{C, 0, 1}, {JZ, 3, 0}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
+ static const O effect[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 3), 0}, {ROW, 0, 0}, {XS, 1, 0}, {C, 0, 1},
+ {PURE_OP_REQUIRE, 0, 0}};
+ static const O short_sum[] = {{FOLD, FOLDING(PURE_FOLD_SUM, 0, 1), 0}, {C, 0, 1}};
+ static const O outside[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 5), 0}, {C, 0, 1}};
+ static const O not_list[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 1, 1), 0}, {C, 0, 1}};
+ static const struct { const O *code; size_t count, length; bool handler; const char *reason; } refused[] = {
+ {nested, LEN(nested), 3, false, "fold inside a fold"},
+ {into, LEN(into), 4, false, "branch into a fold"},
+ {effect, LEN(effect), 5, true, "fold with an effect"},
+ {short_sum, LEN(short_sum), 2, false, "fold block must leave its numbers"},
+ {outside, LEN(outside), 2, false, "fold block outside its block"},
+ {not_list, LEN(not_list), 2, false, "no such state"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i) {
+ V view = {"v", 0, (uint32_t)refused[i].length};
+ H handler = {3, 0, (uint32_t)refused[i].length};
+ Spec spec = refused[i].handler ? fold_spec(refused[i].code, refused[i].count, NULL, 0, &handler, 1)
+ : fold_spec(refused[i].code, refused[i].count, &view, 1, NULL, 0);
+ if (load(spec, &module_b) != PURE_INVALID || !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "fold case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+ }
+}
+
+/* A filtered table: one row per item its filter keeps; its columns read the
+ * item (ROW is the item, not the row), and hosts turn rows into items and
+ * back. The rows follow every change. */
+static const O filter_code[] = {
+ {TI, 0, 0}, {C, 0, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */
+ {XL, 1, 0}, {XL, 1, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, {IS, F_DONE, 0}, /* 7 toggle */
+ {ROW, 0, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, /* 12 filter: open */
+ {ROW, 0, 0}, {IG, F_TITLE, 0}, /* 15 title */
+ {ROW, 0, 0}, {XN, 0, 0}, /* 17 pos */
+};
+enum { FILTERED = 2 | 1 << 8 }; /* a table over state 0, filtered */
+static const W filter_views[] = {{"open", FILTERED, 0, 12, 3}, {"title", 1, 1, 15, 2}, {"pos", 0, 1, 17, 2}};
+
+static Spec filter_spec(const O *code, size_t count, const W *views, size_t nviews) {
+ return (Spec){NULL, 0, fold_inputs, LEN(fold_inputs), NULL, 0, fold_handlers, 2, code, count, NULL, 0,
+ fold_states, 2, fold_constants, 1, NULL, 0, fold_records, 1, views, nviews};
+}
+
+static const char *cell_title(PureApp *app, uint32_t column, size_t row) {
+ static Text text;
+ text = (Text){{0}, 0};
+ CHECK(pure_app_cell_text(app, column, row, text_span, &text) == PURE_OK && text.size < sizeof(text.text));
+ text.text[text.size] = 0;
+ return text.text;
+}
+
+static void filters(void) {
+ CHECK(load(filter_spec(filter_code, LEN(filter_code), filter_views, 3), &module_a) == PURE_OK);
+ PureApp app;
+ double value;
+ size_t at;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK && pure_app_rows(&app, 0) == 0);
+ for (const char *word = "abc"; *word; ++word)
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)word, 1}) == PURE_OK);
+ CHECK(pure_app_rows(&app, 0) == 3 && !strcmp(cell_title(&app, 1, 2), "c") && pure_app_cell(&app, 2, 2, &value) && value == 2);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); /* c is done: it leaves */
+ CHECK(pure_app_rows(&app, 0) == 2 && !pure_app_cell(&app, 2, 2, &value) && !pure_app_item_row(&app, 0, 2, &at));
+ CHECK(pure_app_point(&app, 1, 0) == PURE_OK && pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); /* a too */
+ CHECK(pure_app_rows(&app, 0) == 1 && !strcmp(cell_title(&app, 1, 0), "b"));
+ CHECK(pure_app_cell(&app, 2, 0, &value) && value == 1); /* the item's position, not the row's */
+ CHECK(pure_app_row_item(&app, 0, 0, &at) && at == 1 && !pure_app_row_item(&app, 0, 1, &at));
+ CHECK(pure_app_item_row(&app, 0, 1, &at) && at == 0 && !pure_app_item_row(&app, 0, 0, &at));
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); /* a comes back, before b */
+ CHECK(pure_app_rows(&app, 0) == 2 && !strcmp(cell_title(&app, 1, 0), "a") && pure_app_item_row(&app, 0, 1, &at) && at == 1);
+ CHECK(pure_app_replace(&app, &module_a) == PURE_OK && pure_app_rows(&app, 0) == 2);
+ pure_app_stop(&app);
+
+ /* What a filter may not be. */
+ static const O effect[] = {{ROW, 0, 0}, {XS, 1, 0}, {C, 0, 1}};
+ static const O text_filter[] = {{ROW, 0, 0}, {IG, F_TITLE, 0}};
+ static const O folded[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
+ static const W filtered_by[] = {{"open", FILTERED, 0, 0, 0}}, open3[] = {{"open", FILTERED, 0, 0, 3}},
+ open2[] = {{"open", FILTERED, 0, 0, 2}};
+ static const W not_a_list[] = {{"open", 2 | 2 << 8, 0, 0, 3}};
+ static const W flagged_column[] = {{"open", FILTERED, 0, 0, 3}, {"c", 0 | 1 << 8, 1, 0, 3}};
+ static const struct { const O *code; size_t count; const W *views; size_t nviews; const char *reason; } refused[] = {
+ {effect, LEN(effect), open3, 1, "filter with an effect"},
+ {text_filter, LEN(text_filter), open2, 1, "filter must leave one number"},
+ {folded, LEN(folded), open2, 1, "fold inside a filter"},
+ {effect, LEN(effect), filtered_by, 1, "table list"}, /* a filtered table needs its block */
+ {effect, LEN(effect), not_a_list, 1, "table list"}, /* state 1 is an index */
+ {effect, LEN(effect), flagged_column, 2, "view type"}, /* only a table has a list byte */
+ };
+ for (size_t i = 0; i < LEN(refused); ++i)
+ if (load(filter_spec(refused[i].code, refused[i].count, refused[i].views, refused[i].nviews), &module_b) != PURE_INVALID ||
+ !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "filter case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+}
+
+/* Damaged fold and filter modules: whatever loads runs safely. */
+static void fold_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, fold_spec(fold_code, LEN(fold_code), fold_views, LEN(fold_views), fold_handlers, 2));
+ for (unsigned kind = 0; kind < 2; ++kind) {
+ if (kind) size = build(valid, filter_spec(filter_code, LEN(filter_code), filter_views, 3));
+ uint32_t state = kind ? 0x165667b1u : 0x27d4eb2fu;
+ unsigned accepted = 0;
+ for (unsigned round = 0; round < 20000; ++round) {
+ memcpy(bytes, valid, size);
+ for (unsigned f = 0, flips = 1 + next_random(&state) % 3; 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 < 16; ++e) {
+ uint32_t input = next_random(&state) % 2;
+ if (module_b.input[input].kind == PURE_KIND_TEXT) pure_app_text(&app, input, (PureSpan){(const unsigned char *)"ab", 2});
+ else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN);
+ for (uint32_t v = 0; v < module_b.views; ++v) {
+ double value;
+ size_t item;
+ if (module_b.view_type[v] == PURE_VIEW_TABLE) {
+ size_t rows = pure_app_rows(&app, v);
+ for (size_t row = 0; row < rows + 1; ++row) pure_app_row_item(&app, v, row, &item);
+ } else if (module_b.view_table[v]) {
+ for (size_t row = 0; row < 4; ++row)
+ if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_cell_text(&app, v, row, ignore_span, NULL);
+ else pure_app_cell(&app, v, row, &value);
+ } else 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);
+ }
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"%s_module_mutations\":20000,\"accepted_after_damage\":%u}\n", kind ? "filter" : "fold", accepted);
+ }
+}
+
+/* A host points at an item: the index a view shows as its position moves
+ * there, as an event with no handler, saved like a key event; deferred, it
+ * joins the burst and a failed save undoes it with the rest. */
+static void pointing(void) {
+ CHECK(load(list_spec, &module_a) == PURE_OK);
+ CHECK(pure_module_position(&module_a, 1) == 2); /* position = at.position */
+ CHECK(pure_module_position(&module_a, 0) == -1 && pure_module_position(&module_a, 2) == -1 &&
+ pure_module_position(&module_a, 3) == -1 && pure_module_position(&module_a, 99) == -1);
+ char path[] = "/tmp/pure-point-XXXXXX";
+ CHECK(mkdtemp(path) && pure_store_init(path) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_app_point(&app, 2, 0) == PURE_NOT_FOUND); /* nothing to point at */
+ for (int i = 0; i < 3; ++i) CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"a", 1}) == PURE_OK);
+ uint64_t events = app.events;
+ CHECK(app.values[2] == 2 && pure_app_point(&app, 2, 0) == PURE_OK && app.values[2] == 0 && app.events == events + 1);
+ CHECK(pure_app_point(&app, 2, 3) == PURE_NOT_FOUND && app.values[2] == 0);
+ CHECK(pure_app_point(&app, 0, 1) == PURE_INVALID && pure_app_point(&app, 1, 1) == PURE_INVALID); /* a number, a list */
+ CHECK(pure_app_point(&app, 2, 1) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[2] == 1);
+ CHECK(pure_app_defer(&app, true) && pure_app_point(&app, 2, 2) == PURE_OK && app.open && app.values[2] == 2);
+ CHECK(pure_app_save(&app) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[2] == 2);
+ pure_app_stop(&app);
+ remove_store(path);
+}
+
+/* Keyed lists: items in key order (texts by bytes, numbers by value), each
+ * key once; found by key; a list that becomes keyed is put in order with
+ * its indices following their items, or refused when two items share a key. */
+enum { FIND = PURE_OP_LIST_FIND, HAS = PURE_OP_LIST_HAS, TCMP = PURE_OP_TEXT_COMPARE,
+ PEOPLE = 1, K_AT = 2, SCORES = 3, K_NAME = 0, K_VISITS = 1u << 16 };
+static const S keyed_nums[] = {{"n", 0}};
+static const T keyed_states[] = {{"people", 3 | 1 << 8, 0}, {"at", 4, 1}, {"scores", 3 | 1 << 8, 1}};
+static const T unkeyed_states[] = {{"people", 3, 0}, {"at", 4, 1}, {"scores", 3, 1}};
+static const RF person_fields[] = {{"name", 1, 0}, {"visits", 0, 0}};
+static const RF score_fields[] = {{"score", 0, 0}};
+static const R keyed_records[] = {{"person", person_fields, 2}, {"score", score_fields, 1}};
+static const char *const keyed_constants[] = {"", "bob", "m"};
+static const I keyed_inputs[] = {
+ {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, {"seek", NULL, PURE_KIND_TEXT, false, NULL, 0},
+ {"has", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("drop", "Delete"), KEY("bump", "KeyB"),
+ KEY("two", "KeyN"), KEY("zero", "KeyO"), KEY("minus_zero", "KeyM"), {"twice", NULL, PURE_KIND_TEXT, false, NULL, 0},
+};
+static const O keyed_code[] = {
+ {TI, 0, 0}, {C, 0, 0}, {LA, PEOPLE, 0}, {TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}, /* 0 typed: add, point at it */
+ {TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}, /* 6 seek */
+ {TI, 0, 0}, {HAS, PEOPLE, 0}, {ST, 0, 0}, /* 9 has */
+ {XL, K_AT, 0}, {LR, PEOPLE, 0}, /* 12 drop */
+ {XL, K_AT, 0}, {XL, K_AT, 0}, {IG, PEOPLE | K_VISITS, 0}, {C, 0, 1}, {ADD, 0, 0}, /* 14 bump */
+ {IS, PEOPLE | K_VISITS, 0},
+ {C, 0, 2}, {LA, SCORES, 0}, {C, 0, 0}, {LA, SCORES, 0}, {C, 0, -0.0}, {LA, SCORES, 0}, /* 20 two, zero, minus_zero */
+ {TI, 0, 0}, {C, 0, 0}, {LA, PEOPLE, 0}, {TI, 0, 0}, {C, 0, 0}, {LA, PEOPLE, 0}, /* 26 twice: always exists */
+ {LC, PEOPLE, 0}, /* 32 view count */
+ {XL, K_AT, 0}, {XN, 0, 0}, /* 33 view position */
+ {XL, K_AT, 0}, {IG, PEOPLE | K_NAME, 0}, {TK, 1, 0}, {TCMP, PURE_EQUAL, 0}, /* 35 view is_bob */
+ {XL, K_AT, 0}, {IG, PEOPLE | K_NAME, 0}, {TK, 2, 0}, {TCMP, PURE_LESS, 0}, /* 39 view before_m */
+ {LC, SCORES, 0}, /* 43 view scores */
+ {XL, K_AT, 0}, {IG, PEOPLE | K_NAME, 0}, /* 44 view current */
+};
+static const H keyed_handlers[] = {{1, 0, 6}, {3, 6, 3}, {5, 9, 3}, {7, 12, 2}, {9, 14, 6}, {11, 20, 2}, {13, 22, 2},
+ {15, 24, 2}, {17, 26, 6}};
+static const V keyed_views[] = {{"count", 32, 1}, {"position", 33, 2}, {"is_bob", 35, 4}, {"before_m", 39, 4},
+ {"scores_count", 43, 1}};
+static const V keyed_text_views[] = {{"current", 44, 2}};
+/* Unkeyed, the same states: typed appends and points at the new last item. */
+static const O unkeyed_code[] = {{TI, 0, 0}, {C, 0, 0}, {LA, PEOPLE, 0}, {XE, PEOPLE, 0}, {XS, K_AT, 0}};
+static const H unkeyed_handlers[] = {{1, 0, 5}};
+
+static Spec keyed_spec(const T *states, const O *code, size_t count, const H *handlers, size_t nhandlers) {
+ bool views = code == keyed_code;
+ return (Spec){keyed_nums, 1, keyed_inputs, LEN(keyed_inputs), keyed_views, views ? LEN(keyed_views) : 0, handlers,
+ nhandlers, code, count, NULL, 0, states, LEN(keyed_states), keyed_constants, LEN(keyed_constants),
+ keyed_text_views, views ? 1 : 0, keyed_records, 2, NULL, 0};
+}
+
+static PureResult type_text(PureApp *app, uint32_t input, const char *text) {
+ return pure_app_text(app, input, (PureSpan){(const unsigned char *)text, strlen(text)});
+}
+
+/* A list's first field (text) of every item, comma separated. */
+static const char *names_of(const PureApp *app, uint32_t list) {
+ static char out[512];
+ size_t used = 0;
+ for (size_t at = 0; at < pure_list_count(app->lists[list]); ++at) {
+ PureCell cell = pure_list_get(app->lists[list], at, 0);
+ CHECK(used + cell.size + 2 < sizeof(out));
+ if (at) out[used++] = ',';
+ if (cell.size) memcpy(out + used, cell.text, cell.size);
+ used += cell.size;
+ }
+ out[used] = 0;
+ return out;
+}
+
+static void keyed(void) {
+ CHECK(load(keyed_spec(keyed_states, keyed_code, LEN(keyed_code), keyed_handlers, LEN(keyed_handlers)), &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ for (const char *const *word = (const char *const[]){"m", "b", "z", "a", NULL}; *word; ++word)
+ CHECK(type_text(&app, 0, *word) == PURE_OK);
+ CHECK(!strcmp(names_of(&app, PEOPLE), "a,b,m,z") && app.values[K_AT] == 0 && !strcmp(view_text(&app, 5), "a"));
+ /* A key once: the add fails, and so does an event whose second add repeats its first. */
+ CHECK(type_text(&app, 0, "b") == PURE_CONFLICT && app.fault == PURE_FAULT_EXISTS);
+ CHECK(type_text(&app, 8, "c") == PURE_CONFLICT && !strcmp(names_of(&app, PEOPLE), "a,b,m,z") && app.values[K_AT] == 0);
+ CHECK(type_text(&app, 1, "z") == PURE_OK && app.values[K_AT] == 3 && !strcmp(view_text(&app, 5), "z"));
+ CHECK(type_text(&app, 1, "q") == PURE_NOT_FOUND && app.fault == PURE_FAULT_NOT_FOUND && app.values[K_AT] == 3);
+ CHECK(type_text(&app, 2, "m") == PURE_OK && app.values[0] == 1);
+ CHECK(type_text(&app, 2, "c") == PURE_OK && app.values[0] == 0);
+ CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && pure_list_get(app.lists[PEOPLE], 3, 1).num == 1);
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && !strcmp(names_of(&app, PEOPLE), "a,b,m"));
+ CHECK(app.values[K_AT] == 2 && number_view(&app, 2) == 0 && number_view(&app, 3) == 0); /* "m": not bob, not before m */
+ CHECK(type_text(&app, 1, "b") == PURE_OK && number_view(&app, 2) == 0 && number_view(&app, 3) == 1);
+ CHECK(type_text(&app, 0, "bob") == PURE_OK && number_view(&app, 2) == 1 && number_view(&app, 3) == 1);
+ /* Bytes order texts: a text before the longer ones it begins, UTF-8 after ASCII, the empty text first. */
+ CHECK(type_text(&app, 0, "\xea\xb0\x80") == PURE_OK && type_text(&app, 0, "ab") == PURE_OK);
+ CHECK(type_text(&app, 0, "") == PURE_OK && type_text(&app, 0, "") == PURE_CONFLICT);
+ CHECK(!strcmp(names_of(&app, PEOPLE), ",a,ab,b,bob,m,\xea\xb0\x80") && app.values[K_AT] == 0);
+ /* Numbers order by value, and -0 is the key 0. */
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 6, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(pure_app_input(&app, 7, PURE_EDGE_DOWN) == PURE_CONFLICT && number_view(&app, 4) == 2);
+ CHECK(pure_list_get(app.lists[SCORES], 0, 0).num == 0 && !signbit(pure_list_get(app.lists[SCORES], 0, 0).num) &&
+ pure_list_get(app.lists[SCORES], 1, 0).num == 2);
+
+ /* Replacing: unkeyed, the list stays as it is and takes appends at its end. */
+ CHECK(load(keyed_spec(unkeyed_states, unkeyed_code, LEN(unkeyed_code), unkeyed_handlers, 1), &module_b) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK && type_text(&app, 0, "c") == PURE_OK);
+ CHECK(!strcmp(names_of(&app, PEOPLE), ",a,ab,b,bob,m,\xea\xb0\x80,c") && app.values[K_AT] == 7);
+ CHECK(pure_app_point(&app, K_AT, 6) == PURE_OK); /* at the Korean name */
+ /* Keyed again: put in order, the index following its item. */
+ CHECK(pure_app_replace(&app, &module_a) == PURE_OK && !strcmp(names_of(&app, PEOPLE), ",a,ab,b,bob,c,m,\xea\xb0\x80"));
+ CHECK(app.values[K_AT] == 7 && !strcmp(view_text(&app, 5), "\xea\xb0\x80"));
+ /* Two items with one key cannot become keyed: the app keeps its module and items. */
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK && type_text(&app, 0, "a") == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_a) == PURE_CONFLICT && app.module == &module_b && pure_list_count(app.lists[PEOPLE]) == 9);
+ pure_app_stop(&app);
+
+ /* Stored: a list that becomes keyed is stored in order, once. */
+ char *path = new_store();
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK);
+ for (const char *const *word = (const char *const[]){"m", "b", "z", NULL}; *word; ++word)
+ CHECK(type_text(&app, 0, *word) == PURE_OK);
+ CHECK(pure_app_point(&app, K_AT, 1) == PURE_OK);
+ pure_app_stop(&app);
+ for (int round = 0; round < 2; ++round) {
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ CHECK(!strcmp(names_of(&app, PEOPLE), "b,m,z") && app.values[K_AT] == 0);
+ pure_app_stop(&app);
+ }
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && type_text(&app, 0, "m") == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_CONFLICT);
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && !strcmp(names_of(&app, PEOPLE), "b,m,z,m"));
+ pure_app_stop(&app);
+ remove_store(path);
+ static const char *const words[] = {"a", "b", "ab", "", "\xea\xb0\x80", "zz"};
+ for (uint32_t seed = 1; seed <= 3; ++seed)
+ stored_equivalence(keyed_spec(keyed_states, keyed_code, LEN(keyed_code), keyed_handlers, LEN(keyed_handlers)),
+ words, LEN(words), seed * 0x9e3779b9u);
+
+ /* What a keyed list and a text comparison may not be. */
+ static const O insert[] = {{XF, PEOPLE, 0}, {TK, 0, 0}, {C, 0, 0}, {LI, PEOPLE, 0}};
+ static const O set_key[] = {{XL, K_AT, 0}, {TK, 0, 0}, {IS, PEOPLE | K_NAME, 0}};
+ static const O find_number[] = {{C, 0, 1}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}};
+ static const O find[] = {{TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}};
+ static const O relation[] = {{TI, 0, 0}, {TI, 0, 0}, {TCMP, PURE_NOT_EQUAL + 1, 0}, {ST, 0, 0}};
+ static const O compare_numbers[] = {{C, 0, 1}, {C, 0, 1}, {TCMP, PURE_EQUAL, 0}, {ST, 0, 0}};
+ static const T far_key[] = {{"people", 3 | 3 << 8, 0}, {"at", 4, 1}, {"scores", 3, 1}};
+ static const T keyed_index[] = {{"people", 3, 0}, {"at", 4 | 1 << 8, 1}, {"scores", 3, 1}};
+ static const struct { const T *states; const O *code; size_t count; const char *reason; } refused[] = {
+ {keyed_states, insert, LEN(insert), "insert by position into a keyed list"},
+ {keyed_states, set_key, LEN(set_key), "change of a keyed list's key"},
+ {keyed_states, find_number, LEN(find_number), "operand type"},
+ {unkeyed_states, find, LEN(find), "no such state"},
+ {keyed_states, relation, LEN(relation), "no such state"},
+ {keyed_states, compare_numbers, LEN(compare_numbers), "operand type"},
+ {far_key, unkeyed_code, LEN(unkeyed_code), "state reference"},
+ {keyed_index, unkeyed_code, LEN(unkeyed_code), "state reference"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i) {
+ H handler = {1, 0, (uint32_t)refused[i].count};
+ if (load(keyed_spec(refused[i].states, refused[i].code, refused[i].count, &handler, 1), &module_b) != PURE_INVALID ||
+ !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "keyed case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+ }
+}
+
+/* Damaged keyed modules: whatever loads keeps every keyed list in strict key
+ * order and every index on an item. */
+static void keyed_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, keyed_spec(keyed_states, keyed_code, LEN(keyed_code), keyed_handlers, LEN(keyed_handlers)));
+ static const char *const words[] = {"a", "b", "ab", "", "\xea\xb0\x80"};
+ uint32_t state = 0x6c8e9cf5u;
+ unsigned accepted = 0;
+ for (unsigned round = 0; round < 20000; ++round) {
+ memcpy(bytes, valid, size);
+ for (unsigned f = 0, flips = 1 + next_random(&state) % 3; 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) % module_b.inputs;
+ if (module_b.input[input].kind == PURE_KIND_TEXT) type_text(&app, input, words[next_random(&state) % LEN(words)]);
+ else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN);
+ for (uint32_t v = 0; v < module_b.views; ++v) {
+ double value;
+ if (module_b.view_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));
+ }
+ if (module_b.state_type[s] != PURE_STATE_LIST || !module_b.state_key[s]) continue;
+ unsigned key = module_b.state_key[s] - 1u;
+ bool text = module_b.record_field_text[module_b.record[module_b.state_ref[s]].first + key];
+ for (size_t at = 1; at < pure_list_count(app.lists[s]); ++at) {
+ PureCell a = pure_list_get(app.lists[s], at - 1, key), b = pure_list_get(app.lists[s], at, key);
+ if (!text) { CHECK(a.num < b.num); continue; }
+ size_t shorter = a.size < b.size ? a.size : b.size;
+ int order = shorter ? memcmp(a.text, b.text, shorter) : 0;
+ CHECK(order < 0 || (!order && a.size < b.size));
+ }
+ }
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"keyed_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
+/* ---- Snapshots. Everything an app's states and views show, as text (safe
+ * on any thread): a reader of a snapshot must show what the app showed. */
+typedef struct { char *text; size_t used, size; } Shown;
+
+static bool show_span(void *context, PureSpan span) {
+ Shown *s = context;
+ CHECK(s->used + span.size < s->size);
+ if (span.size) memcpy(s->text + s->used, span.data, span.size);
+ s->used += span.size;
+ return true;
+}
+
+static void show(Shown *s, const char *format, double value) {
+ int n = snprintf(s->text + s->used, s->size - s->used, format, value);
+ CHECK(n >= 0 && (size_t)n < s->size - s->used);
+ s->used += (size_t)n;
+}
+
+static void describe(PureApp *app, Shown *s) {
+ const PureModule *m = app->module;
+ s->used = 0;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (m->state_type[i] == PURE_STATE_TEXT)
+ CHECK(pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), show_span, s) == PURE_OK);
+ else if (m->state_type[i] == PURE_STATE_LIST) {
+ const PureRecord *r = &m->record[m->state_ref[i]];
+ for (size_t at = 0; at < pure_list_count(app->lists[i]); ++at)
+ for (unsigned f = 0; f < r->count; ++f) {
+ PureCell cell = pure_list_get(app->lists[i], at, f);
+ if (m->record_field_text[r->first + f]) show_span(s, (PureSpan){cell.text, cell.size});
+ else show(s, "%g", cell.num);
+ show(s, f + 1 < r->count ? "," : ";", 0);
+ }
+ } else show(s, "%g", app->values[i]);
+ show(s, "|", 0);
+ }
+ for (uint32_t v = 0; v < m->views; ++v) {
+ double value;
+ if (m->view_table[v] || m->view_type[v] == PURE_VIEW_TABLE) continue;
+ if (m->view_type[v] == PURE_VIEW_TEXT) {
+ if (pure_app_view_text(app, v, show_span, s) != PURE_OK) show(s, "!", 0);
+ } else if (pure_app_view(app, v, &value)) show(s, "%g", value);
+ else show(s, "!", 0);
+ show(s, "|", 0);
+ }
+ s->text[s->used] = 0;
+}
+
+/* One random event of the keyed app (see keyed), saved as it returns. */
+static void keyed_event(PureApp *app, uint32_t *seed) {
+ static const char *const names[] = {"a", "b", "ab", "", "\xea\xb0\x80", "zz", "bob", "m", "q", "c"};
+ uint32_t input = next_random(seed) % app->module->inputs;
+ if (app->module->input[input].kind == PURE_KIND_TEXT) type_text(app, input, names[next_random(seed) % LEN(names)]);
+ else pure_app_input(app, input, PURE_EDGE_DOWN);
+}
+
+/* A snapshot shows the states of the save before it, as they were, however
+ * the app goes on (items it changes or drops, a module it switches to, even
+ * after it stops and its store closes); a change not saved yet refuses one,
+ * and a save that fails is never shown. */
+static size_t snapshot_retired, snapshot_reads;
+
+static void snapshots_kept(Spec spec, const Spec *other, void (*event)(PureApp *, uint32_t *), unsigned steps) {
+ enum { KEPT = 8, SIZE = 1 << 16 };
+ static char texts[KEPT + 2][SIZE];
+ char *path = new_store();
+ PureStore *store;
+ CHECK(load(spec, &module_a) == PURE_OK && pure_store_open(path, &store) == PURE_OK);
+ CHECK(!other || load(*other, &module_b) == PURE_OK);
+ PureApp app, reader = {0};
+ CHECK(pure_app_start(&app, &module_a, pure_store_data(store)) == PURE_OK);
+ PureSnapshot *kept[KEPT] = {NULL};
+ Shown seen[KEPT], now = {texts[KEPT], 0, SIZE}, shown = {texts[KEPT + 1], 0, SIZE};
+ for (unsigned k = 0; k < KEPT; ++k) seen[k] = (Shown){texts[k], 0, SIZE};
+ uint32_t seed = 0x51a95u;
+ for (unsigned step = 0; step < steps; ++step) {
+ /* Late on, another module (the keyed app unkeyed), and back if it can. */
+ if (other && (step == steps - 400 || step == steps - 300))
+ CHECK(pure_app_replace(&app, app.module == &module_a ? &module_b : &module_a) != PURE_INVALID);
+ event(&app, &seed);
+ snapshot_retired += app.retired.count;
+ PureSnapshot *made = pure_app_snapshot(&app);
+ CHECK(made);
+ describe(&app, &now);
+ pure_app_read(&reader, made);
+ describe(&reader, &shown);
+ CHECK(!strcmp(now.text, shown.text));
+ unsigned slot = next_random(&seed) % KEPT;
+ if (kept[slot]) {
+ pure_app_read(&reader, kept[slot]);
+ describe(&reader, &shown);
+ CHECK(!strcmp(seen[slot].text, shown.text));
+ pure_snapshot_release(kept[slot]);
+ }
+ kept[slot] = made;
+ memcpy(seen[slot].text, now.text, now.used + 1);
+ }
+ /* Deferred: an unsaved change refuses a snapshot, and a failed save
+ * leaves the one before it true. */
+ CHECK(pure_app_defer(&app, true));
+ describe(&app, &now);
+ PureResult saved = PURE_OK;
+ for (unsigned tries = 0; tries < 64 && saved == PURE_OK; ++tries) {
+ event(&app, &seed);
+ if (!app.open) continue;
+ CHECK(!pure_app_snapshot(&app));
+ fail_stage = PURE_AFTER_HEADER;
+ saved = pure_app_save(&app); /* OK when the event changed nothing */
+ fail_stage = -1;
+ }
+ CHECK(saved == PURE_STORE_ERROR);
+ PureSnapshot *made = pure_app_snapshot(&app);
+ CHECK(made);
+ pure_app_read(&reader, made);
+ describe(&reader, &shown);
+ CHECK(!strcmp(now.text, shown.text));
+ pure_snapshot_release(made);
+ /* The kept ones outlive the app and its store. */
+ pure_app_stop(&app);
+ pure_store_close(store);
+ for (unsigned k = 0; k < KEPT; ++k) {
+ pure_app_read(&reader, kept[k]);
+ describe(&reader, &shown);
+ CHECK(!strcmp(seen[k].text, shown.text));
+ pure_snapshot_release(kept[k]);
+ }
+ pure_app_stop(&reader);
+ remove_store(path);
+}
+
+/* Readers on two threads take the newest snapshot while the app applies
+ * events and makes one after each save: each shows what the app showed. */
+typedef struct {
+ pthread_mutex_t lock;
+ PureSnapshot *newest;
+ char *seen;
+ bool done;
+ unsigned reads;
+} Published;
+
+static void *read_newest(void *context) {
+ Published *p = context;
+ PureApp reader = {0};
+ static _Thread_local char a[1 << 16], b[1 << 16];
+ Shown expected = {a, 0, sizeof(a)}, shown = {b, 0, sizeof(b)};
+ for (bool done = false; !done;) {
+ pthread_mutex_lock(&p->lock);
+ PureSnapshot *s = p->newest;
+ if (s) {
+ pure_snapshot_hold(s);
+ strcpy(expected.text, p->seen);
+ }
+ done = p->done;
+ pthread_mutex_unlock(&p->lock);
+ if (!s) continue;
+ pure_app_read(&reader, s);
+ describe(&reader, &shown);
+ CHECK(!strcmp(expected.text, shown.text));
+ pure_snapshot_release(s);
+ __atomic_add_fetch(&p->reads, 1, __ATOMIC_RELAXED);
+ }
+ pure_app_stop(&reader);
+ return NULL;
+}
+
+static void snapshots_read(Spec spec, void (*event)(PureApp *, uint32_t *), unsigned steps) {
+ static char seen[1 << 16], now[1 << 16];
+ char *path = new_store();
+ PureStore *store;
+ CHECK(load(spec, &module_a) == PURE_OK && pure_store_open(path, &store) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_store_data(store)) == PURE_OK);
+ Published p = {.lock = PTHREAD_MUTEX_INITIALIZER, .seen = seen};
+ pthread_t readers[2];
+ for (unsigned r = 0; r < 2; ++r) CHECK(pthread_create(&readers[r], NULL, read_newest, &p) == 0);
+ Shown shown = {now, 0, sizeof(now)};
+ uint32_t seed = 0x7eadu;
+ for (unsigned step = 0; step < steps; ++step) {
+ event(&app, &seed);
+ PureSnapshot *made = pure_app_snapshot(&app);
+ CHECK(made);
+ describe(&app, &shown);
+ pthread_mutex_lock(&p.lock);
+ PureSnapshot *old = p.newest;
+ p.newest = made;
+ memcpy(seen, shown.text, shown.used + 1);
+ pthread_mutex_unlock(&p.lock);
+ pure_snapshot_release(old);
+ }
+ pthread_mutex_lock(&p.lock);
+ p.done = true;
+ pthread_mutex_unlock(&p.lock);
+ for (unsigned r = 0; r < 2; ++r) CHECK(pthread_join(readers[r], NULL) == 0);
+ CHECK(p.reads > steps / 4);
+ snapshot_reads += p.reads;
+ pure_snapshot_release(p.newest);
+ pure_app_stop(&app);
+ pure_store_close(store);
+ remove_store(path);
+}
+
+static void crash_event(PureApp *app, uint32_t *seed) {
+ (void)seed;
+ CHECK(pure_app_input(app, 0, PURE_EDGE_DOWN) == PURE_OK);
+}
+
+static void snapshots(void) {
+ Spec keyed = keyed_spec(keyed_states, keyed_code, LEN(keyed_code), keyed_handlers, LEN(keyed_handlers));
+ Spec crashing = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0,
+ crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0};
+ Spec unkeyed = keyed_spec(unkeyed_states, unkeyed_code, LEN(unkeyed_code), unkeyed_handlers, 1);
+ snapshots_kept(keyed, &unkeyed, keyed_event, 3000);
+ snapshots_kept(crashing, NULL, crash_event, 300);
+ snapshots_read(keyed, keyed_event, 3000);
+ snapshots_read(crashing, crash_event, 300);
+ /* Without stored states (texts only in memory) there is no snapshot. */
+ CHECK(load(crashing, &module_a) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK && !pure_app_snapshot(&app));
+ pure_app_stop(&app);
+ CHECK(snapshot_retired > 100);
+ printf("{\"snapshots\":6600,\"items_retired\":%zu,\"reads_on_threads\":%zu}\n", snapshot_retired, snapshot_reads);
+}
+
+/* A message of a text, a number, bytes and a u32, in that order: the prefix
+ * holds the text's length, the number and the u32; the text's bytes come
+ * first after it and the bytes (the last variable field) take the rest. A
+ * length past the payload, a text that is not UTF-8 or a number that is not
+ * finite fails the message. */
+enum { TSET = PURE_OP_TEXT_SET };
+static void message_fields(void) {
+ static const S nums[] = {{"n", 0}};
+ static const T texts[] = {{"title", 1, 0}};
+ static const char *const constants[] = {""};
+ static const F fields[] = {{"title", PURE_FIELD_TEXT}, {"points", PURE_FIELD_NUMBER}, {"note", PURE_FIELD_BYTES},
+ {"count", PURE_FIELD_U32}};
+ static const I inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, false, fields, 4}};
+ static const O code[] = {{FIELD, 1, 0}, {FIELD, 3, 0}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0},
+ {FIELD, 2, 0}, {REPLY, 0, 0}};
+ static const H handlers[] = {{1, 0, 8}};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, handlers, 1, code, LEN(code), NULL, 0, texts, 1, constants, 1, NULL, 0,
+ NULL, 0, NULL, 0};
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(module_a.input[0].prefix == 16 && module_a.field[0].offset == 0 && module_a.field[1].offset == 4 &&
+ module_a.field[2].offset == PURE_FIELD_REST && module_a.field[3].offset == 12);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ unsigned char payload[64];
+ /* title "가나" (6 bytes), points 2.5, count 3, then the title's bytes and note "xyz". */
+ size_t size = 0;
+ uint32_t title_size = 6, count = 3;
+ double points = 2.5;
+ memcpy(payload, &title_size, 4);
+ memcpy(payload + 4, &points, 8);
+ memcpy(payload + 12, &count, 4);
+ memcpy(payload + 16, "\xea\xb0\x80\xeb\x82\x98" "xyz", 9);
+ size = 25;
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, size}) == PURE_OK);
+ CHECK(app.values[0] == 5.5 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\xeb\x82\x98") && replied(&app, "xyz"));
+ pure_app_finish(&app);
+ /* Empty variable fields. */
+ title_size = 0;
+ memcpy(payload, &title_size, 4);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_OK && !*state_text(&app, 1) && replied(&app, ""));
+ pure_app_finish(&app);
+ /* A title longer than what is there, a title that is not UTF-8, a number that is not finite, a short prefix. */
+ title_size = 10;
+ memcpy(payload, &title_size, 4);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 25}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT);
+ title_size = 2;
+ memcpy(payload, &title_size, 4);
+ memcpy(payload + 16, "\xff\xfe", 2);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 18}) == PURE_INVALID && app.fault == PURE_FAULT_ENCODING);
+ title_size = 0;
+ points = NAN;
+ memcpy(payload, &title_size, 4);
+ memcpy(payload + 4, &points, 8);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_INVALID && app.fault == PURE_FAULT_NON_FINITE);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 15}) == PURE_LIMIT);
+ CHECK(app.values[0] == 5.5 && !*state_text(&app, 1)); /* failed messages changed nothing */
+ pure_app_stop(&app);
+ /* A text field is text and a bytes field bytes. */
+ static const O text_as_bytes[] = {{FIELD, 0, 0}, {LEN_, 0, 0}, {ST, 0, 0}};
+ static const O bytes_as_text[] = {{FIELD, 2, 0}, {TSET, 1, 0}};
+ static const H three[] = {{1, 0, 3}}, two[] = {{1, 0, 2}};
+ Spec bad = spec;
+ bad.o = text_as_bytes; bad.no = 3; bad.h = three;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ bad.o = bytes_as_text; bad.no = 2; bad.h = two;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+
+ /* Placed by the host: the variable fields wherever they are in the
+ * payload (here a JSON body after the prefix), in any order; the
+ * prefix's lengths are not read. */
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ static const char body[] = "{\"note\":\"xyz\",\"title\":\"\xea\xb0\x80\xeb\x82\x98\"}";
+ points = 2.5;
+ title_size = 999;
+ memcpy(payload, &title_size, 4);
+ memcpy(payload + 4, &points, 8);
+ memcpy(payload + 12, &count, 4);
+ memcpy(payload + 16, body, sizeof(body) - 1);
+ size = 16 + sizeof(body) - 1;
+ uint32_t placed[8] = {16 + 23, 6, 0, 0, 16 + 9, 3, 0, 0};
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_OK);
+ CHECK(app.values[0] == 5.5 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\xeb\x82\x98") && replied(&app, "xyz"));
+ pure_app_finish(&app);
+ /* A field that starts or ends past the payload, or a title that is not UTF-8 there. */
+ placed[0] = (uint32_t)size + 1;
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_LIMIT);
+ placed[0] = (uint32_t)size - 2;
+ placed[1] = 3;
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_LIMIT);
+ placed[0] = 16 + 24;
+ placed[1] = 2;
+ CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_INVALID &&
+ app.fault == PURE_FAULT_ENCODING);
+ CHECK(app.values[0] == 5.5);
+ pure_app_stop(&app);
+
+ /* Fields in groups: a name is a path of identifiers (user.name), and
+ * never both a field and a group. */
+ static const F grouped[] = {{"user.name", PURE_FIELD_TEXT}, {"user.age", PURE_FIELD_U32}, {"plan", PURE_FIELD_TEXT}};
+ static const I grouped_inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, false, grouped, 3}};
+ static const O grouped_code[] = {{FIELD, 1, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0}};
+ static const H grouped_handlers[] = {{1, 0, 4}};
+ Spec groups = spec;
+ groups.in = grouped_inputs; groups.o = grouped_code; groups.no = LEN(grouped_code); groups.h = grouped_handlers;
+ CHECK(load(groups, &module_b) == PURE_OK && !strcmp(module_b.field[0].name, "user.name"));
+ static const char *const refused_paths[] = {"user..name", ".user", "user.", "user.1st", "user name"};
+ for (size_t i = 0; i < LEN(refused_paths); ++i) {
+ F renamed[3] = {grouped[0], grouped[1], grouped[2]};
+ renamed[0].name = refused_paths[i];
+ I renamed_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, false, renamed, 3}};
+ groups.in = renamed_inputs;
+ CHECK(load(groups, &module_b) == PURE_INVALID && strstr(error, "message field"));
+ }
+ F both[3] = {{"user", PURE_FIELD_TEXT}, grouped[1], grouped[2]};
+ I both_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, false, both, 3}};
+ groups.in = both_inputs;
+ CHECK(load(groups, &module_b) == PURE_INVALID && strstr(error, "field and group"));
+}
+
+/* ---- Connections between apps (docs/research/078-app-connections.md): a
+ * send is part of its event and is seen once saved; a receiver applies each
+ * numbered message once, counts a refused one as heard, and forgets what a
+ * failed save undid. */
+enum { SEND = PURE_OP_SEND };
+typedef struct { uint32_t output; uint64_t seq; unsigned char payload[64]; size_t size; } Sent;
+typedef struct { Sent items[16]; size_t count; } Outbox;
+
+static bool collect_sent(void *context, uint32_t output, uint64_t seq, PureSpan payload) {
+ Outbox *o = context;
+ CHECK(o->count < 16 && payload.size <= 64);
+ Sent *s = &o->items[o->count++];
+ *s = (Sent){output, seq, {0}, payload.size};
+ memcpy(s->payload, payload.data, payload.size);
+ return true;
+}
+
+static Outbox outbox_of(const PureApp *app) {
+ Outbox o = {.count = 0};
+ pure_app_outbox(app, collect_sent, &o);
+ return o;
+}
+
+/* buy/fail {item: text, count: u32}: the count at 0, then the item. */
+static PureResult buy(PureApp *app, uint32_t input, const char *item, uint32_t count) {
+ unsigned char payload[64];
+ size_t length = strlen(item);
+ memcpy(payload, &count, 4);
+ memcpy(payload + 4, item, length);
+ PureResult result = pure_app_message(app, input, NULL, -1, (PureSpan){payload, 4 + length});
+ pure_app_finish(app);
+ return result;
+}
+
+/* order {item: text, count: u32, total: number}: count, total, then item. */
+static PureResult deliver(PureApp *app, const char *source, uint64_t seq, const char *item, uint32_t count) {
+ unsigned char payload[64];
+ size_t length = strlen(item);
+ double total = 0;
+ memcpy(payload, &count, 4);
+ memcpy(payload + 4, &total, 8);
+ memcpy(payload + 12, item, length);
+ PureResult result = pure_app_deliver(app, 0, source, seq, (PureSpan){payload, 12 + length}, NULL);
+ pure_app_finish(app);
+ return result;
+}
+
+static void links(void) {
+ static const S sender_nums[] = {{"n", 0}};
+ static const F order_fields[] = {{"item", PURE_FIELD_TEXT}, {"count", PURE_FIELD_U32}};
+ static const F ordered_fields[] = {{"item", PURE_FIELD_TEXT}, {"count", PURE_FIELD_U32}, {"total", PURE_FIELD_NUMBER}};
+ static const I sender_inputs[] = {{"buy", NULL, PURE_KIND_MESSAGE, false, order_fields, 2},
+ {"fail", NULL, PURE_KIND_MESSAGE, false, order_fields, 2},
+ {"ordered", NULL, PURE_KIND_OUTPUT, false, ordered_fields, 3}};
+ static const O sender_code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0},
+ {L, 0, 0}, {SEND, 2, 0}, /* buy */
+ {FIELD, 0, 0}, {FIELD, 1, 0}, {C, 0, 0}, {SEND, 2, 0}, {C, 0, 0}, {REQ, 0, 0}}; /* fail */
+ static const H sender_handlers[] = {{1, 0, 8}, {3, 8, 6}};
+ Spec sender = {sender_nums, 1, sender_inputs, 3, NULL, 0, sender_handlers, 2, sender_code, LEN(sender_code),
+ NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ static const S receiver_nums[] = {{"stock", 0}};
+ static const T receiver_texts[] = {{"last", 1, 0}};
+ static const char *const receiver_constants[] = {""};
+ static const I receiver_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, ordered_fields, 3}};
+ static const O receiver_code[] = {{FIELD, 1, 0}, {L, 0, 0}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0},
+ {FIELD, 1, 0}, {C, 0, 100}, {LT, 0, 0}, {REQ, 0, 0}}; /* refuses 100 or more */
+ static const H receiver_handlers[] = {{1, 0, 10}};
+ Spec receiver = {receiver_nums, 1, receiver_inputs, 1, NULL, 0, receiver_handlers, 1, receiver_code,
+ LEN(receiver_code), NULL, 0, receiver_texts, 1, receiver_constants, 1, NULL, 0, NULL, 0, NULL, 0};
+ if (load(sender, &module_a) != PURE_OK || load(receiver, &module_b) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(module_a.input[2].prefix == 12);
+
+ /* In memory: a send is its event's; the receiver applies it once per source. */
+ PureApp a, b;
+ CHECK(pure_app_start(&a, &module_a, (PureData){0}) == PURE_OK && pure_app_start(&b, &module_b, (PureData){0}) == PURE_OK);
+ CHECK(buy(&a, 0, "pen", 2) == PURE_OK && buy(&a, 1, "ink", 3) == PURE_LIMIT);
+ Outbox o = outbox_of(&a);
+ CHECK(o.count == 1 && o.items[0].output == 2 && o.items[0].seq == 1 && o.items[0].size == 15 && a.values[0] == 1);
+ PureSpan sent = {o.items[0].payload, o.items[0].size};
+ CHECK(pure_app_deliver(&b, 0, "orders.ordered", 1, sent, NULL) == PURE_OK && !b.duplicate && b.values[0] == 2 &&
+ !strcmp(state_text(&b, 1), "pen"));
+ pure_app_finish(&b);
+ CHECK(pure_app_deliver(&b, 0, "orders.ordered", 1, sent, NULL) == PURE_OK && b.duplicate && b.values[0] == 2);
+ CHECK(deliver(&b, "other", 1, "cup", 5) == PURE_OK && !b.duplicate && b.values[0] == 7);
+ CHECK(deliver(&b, "orders.ordered", 3, "hat", 1) == PURE_OK && b.values[0] == 8); /* numbers may skip */
+ CHECK(deliver(&b, "orders.ordered", 2, "old", 1) == PURE_OK && b.duplicate && b.values[0] == 8);
+ /* Refused (count 100): delivered all the same, never applied again. */
+ CHECK(deliver(&b, "other", 2, "big", 100) == PURE_LIMIT && b.values[0] == 7 + 1);
+ CHECK(deliver(&b, "other", 2, "big", 1) == PURE_OK && b.duplicate && b.values[0] == 8);
+ /* Bad deliveries change nothing. */
+ CHECK(deliver(&b, "", 4, "x", 1) == PURE_INVALID && deliver(&b, "a b", 4, "x", 1) == PURE_INVALID);
+ CHECK(deliver(&b, "other", 0, "x", 1) == PURE_INVALID && pure_app_deliver(&a, 2, "s", 1, sent, NULL) == PURE_INVALID);
+ CHECK(pure_app_message(&a, 2, NULL, -1, sent) == PURE_INVALID);
+ /* Trimmed, a send is gone at once for the host and from the store at the next save. */
+ pure_app_trim(&a, 2, 1);
+ CHECK(outbox_of(&a).count == 0 && pure_app_save(&a) == PURE_OK && a.sent_count == 0);
+ CHECK(buy(&a, 0, "cap", 1) == PURE_OK && outbox_of(&a).items[0].seq == 2);
+ pure_app_stop(&a);
+ pure_app_stop(&b);
+
+ /* Stored: sends and heard numbers survive restarts; a deferred burst's sends
+ * appear with its save, and a failed save takes them and their numbers back. */
+ char path_a[64], path_b[64];
+ snprintf(path_a, sizeof(path_a), "%s", new_store());
+ snprintf(path_b, sizeof(path_b), "%s", new_store());
+ CHECK(pure_app_start(&a, &module_a, pure_tree_data(path_a)) == PURE_OK);
+ CHECK(pure_app_start(&b, &module_b, pure_tree_data(path_b)) == PURE_OK);
+ CHECK(buy(&a, 0, "pen", 2) == PURE_OK && buy(&a, 0, "ink", 3) == PURE_OK);
+ pure_app_stop(&a);
+ CHECK(pure_app_start(&a, &module_a, pure_tree_data(path_a)) == PURE_OK);
+ o = outbox_of(&a);
+ CHECK(o.count == 2 && o.items[0].seq == 1 && o.items[1].seq == 2 && a.values[0] == 2);
+ CHECK(pure_app_defer(&a, true));
+ for (int i = 0; i < 3; ++i) CHECK(buy(&a, 0, "box", 1) == PURE_OK);
+ CHECK(outbox_of(&a).count == 2 && pure_app_save(&a) == PURE_OK);
+ o = outbox_of(&a);
+ CHECK(o.count == 5 && o.items[4].seq == 5);
+ CHECK(buy(&a, 0, "lost", 1) == PURE_OK);
+ pure_app_trim(&a, 2, 3);
+ fail_stage = PURE_AFTER_HEADER;
+ CHECK(pure_app_save(&a) == PURE_STORE_ERROR);
+ fail_stage = -1;
+ o = outbox_of(&a);
+ CHECK(o.count == 5 && o.items[0].seq == 1 && a.values[0] == 5); /* the trim and the send undone */
+ CHECK(buy(&a, 0, "next", 1) == PURE_OK && pure_app_save(&a) == PURE_OK);
+ o = outbox_of(&a);
+ CHECK(o.count == 6 && o.items[5].seq == 6); /* numbered on from 5 */
+ pure_app_trim(&a, 2, 4);
+ CHECK(pure_app_save(&a) == PURE_OK);
+ pure_app_stop(&a);
+ CHECK(pure_app_start(&a, &module_a, pure_tree_data(path_a)) == PURE_OK);
+ o = outbox_of(&a);
+ CHECK(o.count == 2 && o.items[0].seq == 5 && o.items[1].seq == 6);
+ /* The receiver: heard numbers stay heard after a restart; a refused one too. */
+ CHECK(deliver(&b, "orders.ordered", 1, "pen", 2) == PURE_OK && deliver(&b, "x", 1, "big", 100) == PURE_LIMIT);
+ pure_app_stop(&b);
+ CHECK(pure_app_start(&b, &module_b, pure_tree_data(path_b)) == PURE_OK && b.values[0] == 2);
+ CHECK(deliver(&b, "orders.ordered", 1, "pen", 2) == PURE_OK && b.duplicate && b.values[0] == 2);
+ CHECK(deliver(&b, "x", 1, "big", 1) == PURE_OK && b.duplicate);
+ /* A save that fails forgets what it heard. */
+ CHECK(pure_app_defer(&b, true) && deliver(&b, "orders.ordered", 2, "ink", 3) == PURE_OK && b.values[0] == 5);
+ fail_stage = PURE_AFTER_HEADER;
+ CHECK(pure_app_save(&b) == PURE_STORE_ERROR && b.values[0] == 2);
+ fail_stage = -1;
+ CHECK(deliver(&b, "orders.ordered", 2, "ink", 3) == PURE_OK && !b.duplicate && b.values[0] == 5);
+ CHECK(pure_app_save(&b) == PURE_OK);
+ pure_app_stop(&b);
+
+ /* Replacing: a module without the output is refused while sends are left
+ * on it; once they are trimmed it replaces, and the output's number is
+ * kept for a module that has it again. */
+ static const I plain_inputs[] = {{"buy", NULL, PURE_KIND_MESSAGE, false, order_fields, 2}};
+ static const O plain_code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}};
+ static const H plain_handlers[] = {{1, 0, 4}};
+ Spec plain = {sender_nums, 1, plain_inputs, 1, NULL, 0, plain_handlers, 1, plain_code, LEN(plain_code),
+ NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(plain, &module_b) == PURE_OK);
+ CHECK(pure_app_replace(&a, &module_b) == PURE_CONFLICT && a.module == &module_a);
+ pure_app_trim(&a, 2, 6);
+ CHECK(pure_app_save(&a) == PURE_OK && pure_app_replace(&a, &module_b) == PURE_OK);
+ CHECK(buy(&a, 0, "plain", 1) == PURE_OK && pure_app_save(&a) == PURE_OK);
+ pure_app_stop(&a);
+ CHECK(pure_app_start(&a, &module_b, pure_tree_data(path_a)) == PURE_OK); /* the number kept in the store */
+ CHECK(pure_app_replace(&a, &module_a) == PURE_OK && buy(&a, 0, "back", 1) == PURE_OK && pure_app_save(&a) == PURE_OK);
+ o = outbox_of(&a);
+ CHECK(o.count == 1 && o.items[0].seq == 7);
+ /* A stored send whose output the module lacks refuses the start. */
+ pure_app_stop(&a);
+ CHECK(pure_app_start(&a, &module_b, pure_tree_data(path_a)) == PURE_CONFLICT);
+ remove_store(path_a);
+ remove_store(path_b);
+
+ /* A process that dies at each commit stage leaves an event and its send
+ * together or neither, and a delivery and its effect together or neither. */
+ snprintf(path_a, sizeof(path_a), "%s", new_store());
+ snprintf(path_b, sizeof(path_b), "%s", new_store());
+ CHECK(load(receiver, &module_b) == PURE_OK);
+ uint64_t heard = 0;
+ size_t kept[2] = {0, 0}; /* sends and deliveries a death left in place */
+ for (int round = 0; round < 2; ++round)
+ for (int stage = PURE_AFTER_HEADER; stage <= PURE_BEFORE_DIR_SYNC; ++stage) {
+ pid_t child = fork();
+ CHECK(child >= 0);
+ if (!child) {
+ PureApp doomed, taker;
+ if (pure_app_start(&doomed, &module_a, pure_tree_data(path_a)) != PURE_OK ||
+ pure_app_start(&taker, &module_b, pure_tree_data(path_b)) != PURE_OK) _exit(1);
+ crash = 1;
+ fail_stage = stage;
+ if (round) buy(&doomed, 0, "crash", 1);
+ else deliver(&taker, "orders.ordered", heard + 1, "crash", 1);
+ _exit(0);
+ }
+ int status;
+ CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77);
+ CHECK(pure_app_start(&a, &module_a, pure_tree_data(path_a)) == PURE_OK);
+ CHECK(pure_app_start(&b, &module_b, pure_tree_data(path_b)) == PURE_OK);
+ o = outbox_of(&a);
+ CHECK(o.count == (size_t)a.values[0] && (!o.count || o.items[o.count - 1].seq == o.count));
+ heard = b.heard_count ? b.heard[0].seq : 0;
+ CHECK(b.values[0] == (double)heard);
+ kept[0] = (size_t)heard;
+ kept[1] = o.count;
+ pure_app_stop(&a);
+ pure_app_stop(&b);
+ }
+ CHECK(kept[0] == 1 && kept[1] == 1); /* only a death after the head was written keeps it */
+ remove_store(path_a);
+ remove_store(path_b);
+
+ /* What an output and SEND may not be. */
+ const struct { const O *code; size_t count; const char *reason; } refused[] = {
+ {(const O[]){{FIELD, 1, 0}, {FIELD, 0, 0}, {L, 0, 0}, {SEND, 2, 0}}, 4, "operand type"},
+ {(const O[]){{FIELD, 0, 0}, {FIELD, 1, 0}, {L, 0, 0}, {SEND, 1, 0}}, 4, "no such state"},
+ {(const O[]){{FIELD, 0, 0}, {FIELD, 1, 0}, {SEND, 2, 0}}, 3, "stack underflow"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i) {
+ Spec bad = sender;
+ H handler = {1, 0, (uint32_t)refused[i].count};
+ bad.o = refused[i].code; bad.no = refused[i].count; bad.h = &handler; bad.nh = 1;
+ if (load(bad, &module_b) != PURE_INVALID || !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "links case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+ }
+ H on_output[] = {{1, 0, 8}, {5, 8, 6}};
+ Spec bad = sender;
+ bad.h = on_output;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "handler event"));
+}
+
+/* A timer's tick sees the time the host set; floor and a floored modulo
+ * take it apart. now is for handlers only. */
+enum { NOW = PURE_OP_NOW, FLOOR = PURE_OP_FLOOR, MOD = PURE_OP_MOD };
+static void timers(void) {
+ static const S nums[] = {{"clock", 0}, {"seconds", 0}, {"ticks", 0}, {"rest", 0}};
+ static const I inputs[] = {TIMER("tick", 1000), KEY("left", "KeyL"), KEY("zero", "KeyZ")};
+ static const O code[] = {
+ {NOW, 0, 0}, {ST, 0, 0}, {NOW, 0, 0}, {C, 0, 1000}, {DIV, 0, 0}, {FLOOR, 0, 0}, {C, 0, 60}, {MOD, 0, 0},
+ {ST, 1, 0}, {L, 2, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 2, 0}, /* 0 tick */
+ {C, 0, -7}, {C, 0, 3}, {MOD, 0, 0}, {C, 0, 7}, {C, 0, -3}, {MOD, 0, 0}, {C, 0, 10}, {MUL, 0, 0},
+ {ADD, 0, 0}, {ST, 3, 0}, /* 13 left: 2 + -20 */
+ {C, 0, 1}, {C, 0, 0}, {MOD, 0, 0}, {ST, 3, 0}, /* 23 zero */
+ };
+ static const H handlers[] = {{1, 0, 13}, {3, 13, 10}, {5, 23, 4}};
+ static const V one_view[] = {{"v", 0, 1}};
+ Spec spec = SPEC(nums, inputs, one_view, handlers, code);
+ spec.nv = 0;
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(module_a.input[0].kind == PURE_KIND_TIMER && module_a.input[0].period == 1000);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ app.now = 1700000123456.0;
+ CHECK(pure_app_tick(&app, 0) == PURE_OK && app.values[0] == 1700000123456.0 &&
+ app.values[1] == (double)(1700000123 % 60) && app.values[2] == 1);
+ CHECK(pure_app_tick(&app, 1) == PURE_INVALID && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_INVALID);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[3] == 2 - 20);
+ /* A zero takes the divisor's sign too. */
+ static const O zeros[] = {{C, 0, -6}, {C, 0, 3}, {MOD, 0, 0}, {ST, 0, 0}, {C, 0, 6}, {C, 0, -3}, {MOD, 0, 0}, {ST, 1, 0}};
+ static const H zero_handler[] = {{3, 0, 8}};
+ Spec signs = spec;
+ signs.o = zeros; signs.no = 8; signs.h = zero_handler; signs.nh = 1;
+ CHECK(load(signs, &module_b) == PURE_OK);
+ PureApp zero;
+ CHECK(pure_app_start(&zero, &module_b, (PureData){0}) == PURE_OK && pure_app_input(&zero, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(zero.values[0] == 0 && !signbit(zero.values[0]) && zero.values[1] == 0 && signbit(zero.values[1]));
+ pure_app_stop(&zero);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == PURE_FAULT_DIVIDE);
+ pure_app_stop(&app);
+ /* A timer's period is 1 ms to a day, and it has nothing else; now is for handlers. */
+ static const I no_period[] = {TIMER("tick", 0)}, too_long[] = {TIMER("tick", PURE_TIMER_LONGEST + 1)};
+ static const O now_view[] = {{NOW, 0, 0}};
+ Spec bad = spec;
+ bad.in = no_period; bad.ni = 1; bad.nh = 1; bad.o = code; bad.no = 13;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "timer input"));
+ bad.in = too_long;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "timer input"));
+ bad = SPEC(nums, inputs, one_view, handlers, now_view);
+ bad.nh = 0;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
+}
+
+int main(void) {
+ behaviour();
+ faults();
+ rejections();
+ mutations();
+ messages();
+ message_fields();
+ timers();
+ message_mutations();
+ texts();
+ marks();
+ text_mutations();
+ lists();
+ folds();
+ filters();
+ keyed();
+ keyed_mutations();
+ snapshots();
+ links();
+ fold_mutations();
+ pointing();
+ list_mutations();
+ tables();
+ table_mutations();
+ stored_states();
+ deferred();
+ printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp));
+ puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-text-order-message-fields-timers-now-snapshots-links\"}");
+ return 0;
+}