From 37d3a1e152707d2da587440c4cea8601b9ef0fd2 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 27 Sep 2026 13:14:38 +0100 Subject: Lay Pure out as a core with the parts selected around it The app model, the tree store and the HTTP host lived in prototypes/connections among the earlier document runtime, the store was known only by its format's name, and Vega compiled six Pure sources by path. Now, as the README's principle has it: core/ runtime, flow and the app model (module*.c), with what the core asks of storage (state.h, store.h) store/ the tree store (PURETRE3, PUREREC1) serve/ pure-serve: HTTP host, outbound delivery, JSON in place cli/ pure-run, pure-edit-module A top-level make builds libpure.a (app model and store, for hosts to link) and the hosts; make check runs the store's and the app model's tests. The earlier runtime still builds from prototypes/connections against core/ and store/ (check, check-tree and check-core pass). Design 009 records the rule: parts by role behind libraries; git repositories split by coupling, so the store stays here while its contract moves with the core, and Vega stays apart. --- core/flow.c | 41 + core/flow.h | 25 + core/module-list.c | 428 ++++++++ core/module-list.h | 74 ++ core/module-save.c | 488 +++++++++ core/module-save.h | 36 + core/module-store.c | 88 ++ core/module-store.h | 11 + core/module-text.c | 189 ++++ core/module-text.h | 34 + core/module.c | 2122 ++++++++++++++++++++++++++++++++++++++ core/module.h | 343 +++++++ core/result.h | 8 + core/runtime.c | 394 +++++++ core/runtime.h | 150 +++ core/state.h | 84 ++ core/store.h | 54 + core/test-module.c | 2853 +++++++++++++++++++++++++++++++++++++++++++++++++++ 18 files changed, 7422 insertions(+) create mode 100644 core/flow.c create mode 100644 core/flow.h create mode 100644 core/module-list.c create mode 100644 core/module-list.h create mode 100644 core/module-save.c create mode 100644 core/module-save.h create mode 100644 core/module-store.c create mode 100644 core/module-store.h create mode 100644 core/module-text.c create mode 100644 core/module-text.h create mode 100644 core/module.c create mode 100644 core/module.h create mode 100644 core/result.h create mode 100644 core/runtime.c create mode 100644 core/runtime.h create mode 100644 core/state.h create mode 100644 core/store.h create mode 100644 core/test-module.c (limited to 'core') 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 + +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 +#include + +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 +#include +#include + +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(©->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, ©)) 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 +#include +#include + +/* 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 +#include +#include +#include + +/* 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 +#include +#include +#include + +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 +#include + +/* 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 +#include +#include +#include + +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, ©); + 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 +#include +#include + +/* 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 + +#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 + +/* 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 + +/* 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 + +#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 +#include +#include +#include +#include +#include +#include +#include + +#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; +} -- cgit v1.2.3