aboutsummaryrefslogtreecommitdiffstats
path: root/core/module.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-18 17:23:37 +0100
committereunhhu <[email protected]>2026-10-18 17:23:37 +0100
commitd8e297452d8e0896d4733e09b70fdc3e6ce08a55 (patch)
treeb7502c598faf2fe54f49dc7e40ebfe54f94bb910 /core/module.c
parent241016b0fc3139414089619f0ef8cb7237722c6c (diff)
downloadpure-d8e297452d8e0896d4733e09b70fdc3e6ce08a55.tar.gz
pure-d8e297452d8e0896d4733e09b70fdc3e6ce08a55.zip
Module format and executor: 027's stage 3, nested stored state: a stored or a transient list's records may hold lists (a message's rows not, nor as a key); such a list's cell holds the value in its items' stored form (PURE_CELL_LIST, held as bytes are: each item's fields in order, 8 bytes a number, bool or i64, a u32le length and bytes a text or a list, a list's in this form again), read into one of the event's lists by ITEM_GET (counted in the event's room) and written from one by LIST_APPEND, LIST_INSERT and ITEM_SET, APPEND_IN reading, appending and writing back (undone with the event): images, the log, undoing, readers' shares and commits hold it as a text's bytes, so nothing new is stored or recovered; a list image's header names its record graph (a list field's items' record by its number, the graph's other records after the fields: a record holds itself through a list), and starting or replacing moves the items of the lists in fields by field name down the graph (PureLayout, pure_list_migrate); an event's lists keep the event's lists' numbers; a view lets go the lists a view before it read in; no FIND joined to a list field's read; pure-run shows a kept list's lists; unit tests (an event changing a number and a stored item's list, the process dying at the tree's four stages and the log's two, failed commits changing nothing; a replacement moving a stored and a transient list's lists, their items' record gaining a field and the board's fields changing places; under ASan and UBSan too)
Diffstat (limited to 'core/module.c')
-rw-r--r--core/module.c428
1 files changed, 374 insertions, 54 deletions
diff --git a/core/module.c b/core/module.c
index 19fb7fb..2790137 100644
--- a/core/module.c
+++ b/core/module.c
@@ -412,9 +412,10 @@ static unsigned char state_type(const PureModule *m, uint32_t state) {
return m->state_type[state] == PURE_STATE_BOOL ? T_BOOL : m->state_type[state] == PURE_STATE_I64 ? T_I64 : T_NUM;
}
-/* Whether a list's record field is a text. */
+/* Whether a list's record field's cells are bytes: a text, or a list in a list kept past an event (027's stage 3). */
static bool text_field(const PureModule *m, uint32_t list, uint32_t field) {
- return m->record_field_type[m->record[m->state_ref[list]].first + field] == PURE_RECORD_TEXT;
+ uint8_t type = m->record_field_type[m->record[m->state_ref[list]].first + field];
+ return type == PURE_RECORD_TEXT || (type == PURE_RECORD_LIST && !(m->state_flags[list] & PURE_LIST_ROWS >> 8));
}
/* What a local holds (its argument's bits 8-15): a number, an index, a bool, an i64 or a list (027's 2c). */
@@ -592,13 +593,12 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
REFUSE("change of a bound list");
if (op->op == PURE_OP_LIST_VALUE && !event_list_state(m, arg)) REFUSE("value of a list not an event's");
if (op->op == PURE_OP_LIST_BIND && !bound_list(m, arg)) REFUSE("list bound to a list not bound");
- if (op->op == PURE_OP_APPEND_IN &&
- (!event_list_state(m, list_of(arg)) || field_type(m, list_of(arg), item_field(arg)) != T_LIST))
- REFUSE("append into a field not an event's list's list");
+ if (op->op == PURE_OP_APPEND_IN && field_type(m, list_of(arg), item_field(arg)) != T_LIST)
+ REFUSE("append into a field not a list");
/* A view's body changes an event's lists alone: its states are a snapshot's. */
if (body && (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT || op->op == PURE_OP_LIST_REMOVE ||
- op->op == PURE_OP_ITEM_SET) &&
- !(m->state_flags[op->op == PURE_OP_ITEM_SET ? list_of(arg) : arg] & PURE_LIST_ROWS >> 8))
+ op->op == PURE_OP_ITEM_SET || op->op == PURE_OP_APPEND_IN) &&
+ !(m->state_flags[op->op == PURE_OP_ITEM_SET || op->op == PURE_OP_APPEND_IN ? list_of(arg) : arg] & PURE_LIST_ROWS >> 8))
REFUSE("view changes a state");
/* PICK copies the value arg below the top; SLIDE drops arg values under the top. */
if (kind == A_DEPTH) {
@@ -1814,10 +1814,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
* list is keyed by one; a register is no message's rows. */
for (uint32_t i = 0; i < out->states; ++i) {
if (out->state_type[i] != PURE_STATE_LIST) continue;
- bool nested = out->record_nested[out->state_ref[i]];
- if ((nested && !(out->state_flags[i] & PURE_LIST_ROWS >> 8)) ||
- (out->state_key[i] && out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_LIST))
- REFUSE("list in a record outside an event's list", i);
+ if (out->state_key[i] && out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_LIST)
+ REFUSE("list in a record as a key", i);
}
for (uint32_t f = 0; f < out->fields; ++f)
if (out->field[f].type == PURE_FIELD_ROWS &&
@@ -1989,7 +1987,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
if (out->code[pc].op == PURE_OP_JZ || out->code[pc].op == PURE_OP_JMP) landed[out->code[pc].arg] = true;
for (uint32_t pc = 0; pc + 1 < out->code_count; ++pc) {
PureOp *op = &out->code[pc], *next = op + 1;
- if (op->op == PURE_OP_LIST_FIND && next->op == PURE_OP_ITEM_GET && list_of(next->arg) == op->arg && !landed[pc + 1])
+ if (op->op == PURE_OP_LIST_FIND && next->op == PURE_OP_ITEM_GET && list_of(next->arg) == op->arg && !landed[pc + 1] &&
+ out->record_field_type[out->record[out->state_ref[op->arg]].first + item_field(next->arg)] != PURE_RECORD_LIST)
op->plan |= PLAN_JOINED;
}
/* A FIND whose index waits on the stack for an ITEM_SET of its list,
@@ -2404,7 +2403,8 @@ static int hold_copy(PureApp *app, PureArena *arena, unsigned slot, PureBytes v,
/* ---- Lists in records (027's 2b): the event's lists made for LIST fields, numbered from 1 */
-PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record);
+PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record, bool kept);
+static uint8_t cell_kind(const PureModule *m, uint32_t field, bool kept);
/* The values that share each list in the LIST fields of cells (an item's, just stored). */
static void share_cells(PureApp *app, const PureModule *m, uint32_t record, const PureCell *cells) {
@@ -2425,7 +2425,7 @@ static uint64_t nest(PureApp *app, uint32_t record, const PureList *from) {
app->nested.shared = shared;
app->nested.capacity = capacity;
}
- PureList *list = new_list(m, record);
+ PureList *list = new_list(m, record, false);
if (!list) return 0;
const PureRecord *r = &m->record[record];
for (size_t at = 0, count = from ? pure_list_count(from) : 0; at < count; ++at) {
@@ -2449,6 +2449,183 @@ static void drop_nested(PureApp *app) {
app->nested.count = 0;
}
+/* ---- Lists kept past an event (027's stage 3): a stored or transient list holds a list field's value in its items'
+ * stored form (PURE_CELL_LIST: each item's fields in order, a number's, a bool's and an i64's 8 bytes little-endian, a
+ * text's and a list's u32le length and bytes, a list's in this form again). Read, it becomes one of the event's lists;
+ * set, it is written from one. */
+static bool kept_list(const PureModule *m, uint32_t list) { return !(m->state_flags[list] & PURE_LIST_ROWS >> 8); }
+
+/* The stored form's bytes of the event's list number list (0: an empty one) of record's items. */
+static size_t value_size(const PureApp *app, uint32_t record, uint64_t list) {
+ const PureModule *m = app->module;
+ const PureRecord *r = &m->record[record];
+ const PureList *items = list ? app->nested.list[list - 1] : NULL;
+ size_t size = 0;
+ for (size_t at = 0, count = items ? pure_list_count(items) : 0; at < count; ++at)
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint8_t type = m->record_field_type[r->first + f];
+ PureCell cell = pure_list_get(items, at, f);
+ size += type == PURE_RECORD_TEXT ? 4 + cell.size :
+ type == PURE_RECORD_LIST ? 4 + value_size(app, nested_record(m, r->first + f), (uint64_t)cell.i64) : 8;
+ }
+ return size;
+}
+
+static unsigned char *value_put(const PureApp *app, uint32_t record, uint64_t list, unsigned char *out) {
+ const PureModule *m = app->module;
+ const PureRecord *r = &m->record[record];
+ const PureList *items = list ? app->nested.list[list - 1] : NULL;
+ for (size_t at = 0, count = items ? pure_list_count(items) : 0; at < count; ++at)
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint8_t type = m->record_field_type[r->first + f];
+ PureCell cell = pure_list_get(items, at, f);
+ if (type == PURE_RECORD_TEXT) {
+ write32(out, (uint32_t)cell.size);
+ if (cell.size) memcpy(out + 4, cell.text, cell.size);
+ out += 4 + cell.size;
+ } else if (type == PURE_RECORD_LIST) {
+ unsigned char *end = value_put(app, nested_record(m, r->first + f), (uint64_t)cell.i64, out + 4);
+ write32(out, (uint32_t)(end - out - 4));
+ out = end;
+ } else {
+ write_i64(out, cell.i64);
+ out += 8;
+ }
+ }
+ return out;
+}
+
+/* A list value's stored form (bytes) read into a new one of the event's lists of record's items, its lists in fields
+ * too: its number (0: none), or 0 with *made false where the bytes are not that form or memory ran out (*bad: they are
+ * not). */
+static uint64_t value_take(PureApp *app, uint32_t record, const unsigned char *bytes, size_t size, bool *made, bool *bad) {
+ const PureModule *m = app->module;
+ const PureRecord *r = &m->record[record];
+ *made = true;
+ if (!size) return 0;
+ uint64_t list = nest(app, record, NULL);
+ if (!list) return *made = false, 0;
+ for (size_t at = 0; at < size;) {
+ PureCell cells[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint32_t field = r->first + f;
+ uint8_t type = m->record_field_type[field];
+ if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) {
+ uint32_t length = size - at >= 4 ? read32(bytes + at) : UINT32_MAX;
+ if (length > size - at - 4 || (type == PURE_RECORD_TEXT && !pure_utf8_valid((PureSpan){bytes + at + 4, length})))
+ return *bad = true, *made = false, 0;
+ cells[f] = (PureCell){.text = (unsigned char *)bytes + at + 4, .size = length};
+ if (type == PURE_RECORD_LIST) {
+ cells[f] = (PureCell){.i64 = (int64_t)value_take(app, nested_record(m, field), bytes + at + 4, length, made, bad)};
+ if (!*made) return 0;
+ }
+ at += 4 + length;
+ } else {
+ if (size - at < 8) return *bad = true, *made = false, 0;
+ cells[f] = (PureCell){.i64 = read_i64(bytes + at)};
+ at += 8;
+ if (!pure_cell_fits(pure_record_kind(m, field), cells[f])) return *bad = true, *made = false, 0;
+ }
+ }
+ PureList *items = app->nested.list[list - 1]; /* (the lists in its fields may have moved the table) */
+ if (pure_list_insert(items, pure_list_count(items), cells, NULL) != PURE_OK) return *made = false, 0;
+ if (m->record_nested[record]) share_cells(app, m, record, cells);
+ }
+ return list;
+}
+
+/* An event's list's value as a kept list's cell holds it: its stored form in the arena (room reserved first). */
+static __attribute__((cold, noinline)) PureResult value_cell(PureApp *app, PureArena *arena, uint32_t record, uint64_t list,
+ PureCell *out) {
+ size_t size = value_size(app, record, list);
+ if (!reserve(arena, size)) return PURE_ALLOCATION;
+ unsigned char *at = arena->bytes + arena->used;
+ value_put(app, record, list, at);
+ arena->used += size;
+ *out = (PureCell){.text = at, .size = size};
+ return PURE_OK;
+}
+
+/* An item's cells for a kept list of record's items whose fields hold lists, from the stack (base on): its texts and
+ * its lists' stored forms in the arena, all the room reserved first (no span moves while the next is made). */
+static __attribute__((cold, noinline)) PureResult kept_cells(PureApp *app, PureArena *arena, uint32_t record, const Slot *values,
+ PureCell *cells) {
+ const PureModule *m = app->module;
+ const PureRecord *r = &m->record[record];
+ size_t total = 0;
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint8_t type = m->record_field_type[r->first + f];
+ total += type == PURE_RECORD_LIST ? value_size(app, nested_record(m, r->first + f), (uint64_t)values[f].i64) :
+ type == PURE_RECORD_TEXT ? (size_t)values[f].bytes.length : 0;
+ }
+ if (!reserve(arena, total)) return PURE_ALLOCATION;
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint8_t type = m->record_field_type[r->first + f];
+ PureSpan span = {0};
+ PureResult status = PURE_OK;
+ if (type == PURE_RECORD_LIST) status = value_cell(app, arena, nested_record(m, r->first + f), (uint64_t)values[f].i64, &cells[f]);
+ else if (type == PURE_RECORD_TEXT && (status = contiguous(app, arena, values[f].bytes, &span)) == PURE_OK)
+ cells[f] = (PureCell){.text = (unsigned char *)span.data, .size = span.size};
+ else cells[f] = (PureCell){.i64 = values[f].i64};
+ if (status != PURE_OK) return status;
+ }
+ return PURE_OK;
+}
+
+static PureUndo *undo_for(PureApp *app, uint32_t list);
+
+/* A kept list's list value (a cell's stored form) read into one of the event's lists, counted in the event's room: its
+ * number in *out; BUDGET past the room. */
+static __attribute__((cold, noinline)) PureResult kept_value(PureApp *app, uint32_t list, uint32_t field, PureCell cell,
+ int64_t *out) {
+ const PureModule *m = app->module;
+ bool made, bad = false;
+ uint64_t value = value_take(app, nested_record(m, m->record[m->state_ref[list]].first + field), cell.text, cell.size, &made, &bad);
+ if (!made) return bad ? PURE_STORE_ERROR : PURE_ALLOCATION;
+ if (!grows(app, (Held){0, 0}, (Held){0, cell.size})) return PURE_BUDGET;
+ *out = (int64_t)value;
+ return PURE_OK;
+}
+
+/* APPEND_IN into a kept list's item (at): the list in its field read in, the item (its fields from values) appended,
+ * the list written back (undone with the event). 0 done; 1 a fault (*fault); 2 refused and 3 stopped (*status). */
+static __attribute__((cold, noinline)) int append_kept(PureApp *app, PureArena *arena, uint32_t list, uint32_t field, size_t at,
+ const Slot *values, PureFault *fault, PureResult *status) {
+ const PureModule *m = app->module;
+ uint32_t inner = nested_record(m, m->record[m->state_ref[list]].first + field);
+ const PureRecord *record = &m->record[inner];
+ int64_t value = 0;
+ if ((*status = kept_value(app, list, field, pure_list_get(app->lists[list], at, field), &value)) != PURE_OK)
+ return *status == PURE_BUDGET ? (*fault = PURE_FAULT_MEMORY, 1) : 3;
+ uint64_t own = value ? (uint64_t)value : nest(app, inner, NULL);
+ if (!own) return *status = PURE_ALLOCATION, 3;
+ size_t mark = arena->used, total = 0;
+ for (unsigned f = 0; f < record->count; ++f)
+ if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT) total += (size_t)values[f].bytes.length;
+ if (!reserve(arena, total)) return *status = PURE_ALLOCATION, 3;
+ PureCell cells[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < record->count; ++f) {
+ PureSpan span = {0};
+ if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT &&
+ (*status = contiguous(app, arena, values[f].bytes, &span)) != PURE_OK)
+ return 3;
+ cells[f] = (PureCell){.i64 = values[f].i64, .text = (unsigned char *)span.data, .size = span.size};
+ }
+ PureList *in = app->nested.list[own - 1];
+ *status = pure_list_insert(in, pure_list_count(in), cells, NULL);
+ arena->used = mark;
+ if (*status != PURE_OK) return 2;
+ if (m->record_nested[inner]) share_cells(app, m, inner, cells);
+ PureCell cell;
+ Held before = list_held(app, app->lists[list]);
+ if ((*status = value_cell(app, arena, inner, own, &cell)) != PURE_OK) return 3;
+ *status = pure_list_set(app->lists[list], at, field, cell, undo_for(app, list));
+ arena->used = mark;
+ if (*status != PURE_OK) return 2;
+ if (!grows(app, before, list_held(app, app->lists[list]))) return *fault = PURE_FAULT_MEMORY, 1;
+ return 0;
+}
+
/* NUM_TEXT's text of v in a format (the argument past its type, PureFormat): false where an integer's form meets a
* number that is not a whole one within 2^53. */
static bool formatted(Slot v, uint32_t arg, char *out, size_t *length) {
@@ -3618,11 +3795,13 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
}
/* 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)
+ bool kept = m->record_nested[m->state_ref[arg]] && kept_list(m, arg); /* its lists in their stored form (027's 3) */
+ PureCell cells[PURE_RECORD_FIELDS];
+ if (kept && (status = kept_cells(app, arena, m->state_ref[arg], stack + base, cells)) != PURE_OK) STOPPED(status);
+ for (unsigned f = 0; !kept && f < record->count; ++f)
if (text_field(m, arg, f)) total += (size_t)stack[base + f].bytes.length;
if (!reserve(arena, total)) STOPPED(PURE_ALLOCATION);
- PureCell cells[PURE_RECORD_FIELDS];
- for (unsigned f = 0; f < record->count; ++f) {
+ for (unsigned f = 0; !kept && f < record->count; ++f) {
PureSpan span = {0};
if (text_field(m, arg, f) && (status = contiguous(app, arena, stack[base + f].bytes, &span)) != PURE_OK)
STOPPED(status);
@@ -3643,7 +3822,7 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
if (status == PURE_INVALID) status = pure_list_insert(list, at, cells, undo_for(app, arg));
arena->used = mark;
if (status != PURE_OK) REFUSED;
- if (m->record_nested[m->state_ref[arg]]) share_cells(app, m, m->state_ref[arg], cells);
+ if (m->record_nested[m->state_ref[arg]] && !kept) share_cells(app, m, m->state_ref[arg], cells);
if (m->state_indexed[arg]) move_indices(m, values, arg, pure_list_count(list), at, true);
if (!grows(app, before, list_held(app, list))) { kind = PURE_FAULT_MEMORY; FAILED; }
NEXT;
@@ -3669,7 +3848,13 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
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 (!(op->plan & PLAN_TEXT)) top->i64 = cell.i64; /* the bits: a number's, a bool's or an i64's (else a text) */
- else if ((status = keep_bytes(arena, cell.text, cell.size, &top->bytes)) != PURE_OK) STOPPED(status);
+ else if (m->record_field_type[m->record[m->state_ref[list]].first + field] == PURE_RECORD_LIST) {
+ if ((status = kept_value(app, list, field, cell, &top->i64)) != PURE_OK) { /* a kept list's list: read in */
+ if (status != PURE_BUDGET) STOPPED(status);
+ kind = PURE_FAULT_MEMORY;
+ FAILED;
+ }
+ } else if ((status = keep_bytes(arena, cell.text, cell.size, &top->bytes)) != PURE_OK) STOPPED(status);
NEXT;
}
case PURE_OP_ITEM_SET: {
@@ -3678,9 +3863,12 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
size_t at = (size_t)stack[--sp].num, mark = arena->used;
Held before = list_held(app, app->lists[list]);
PureSpan span = {0};
- status = op->plan & PLAN_TEXT ? contiguous(app, arena, value.bytes, &span) : PURE_OK;
- PureCell cell = {.i64 = value.i64, .text = (unsigned char *)span.data, .size = span.size};
bool nested = m->record_field_type[m->record[m->state_ref[list]].first + field] == PURE_RECORD_LIST;
+ bool kept = nested && op->plan & PLAN_TEXT; /* a kept list's list: its stored form (027's stage 3) */
+ status = kept ? PURE_OK : op->plan & PLAN_TEXT ? contiguous(app, arena, value.bytes, &span) : PURE_OK;
+ PureCell cell = {.i64 = value.i64, .text = (unsigned char *)span.data, .size = span.size};
+ if (kept) status = value_cell(app, arena, nested_record(m, m->record[m->state_ref[list]].first + field), (uint64_t)value.i64, &cell);
+ nested = nested && !kept;
uint64_t was = nested && status == PURE_OK && at < pure_list_count(app->lists[list]) ?
(uint64_t)pure_list_get(app->lists[list], at, field).i64 : 0;
if (status == PURE_OK) /* where its FIND found it (PLAN_ROW: a HASHED list's slot), else its position */
@@ -3947,6 +4135,13 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
size_t base = sp - record->count, at = (size_t)stack[base - 1].num;
sp = base - 1;
if (at >= pure_list_count(app->lists[list])) { kind = PURE_FAULT_NOT_FOUND; FAILED; }
+ if (kept_list(m, list)) { /* a kept list's (027's stage 3): its list read in, the item appended, written back */
+ int how = append_kept(app, arena, list, field, at, stack + base, &kind, &status);
+ if (how == 1) FAILED;
+ if (how == 2) REFUSED;
+ if (how == 3) STOPPED(status);
+ NEXT;
+ }
uint64_t into = (uint64_t)pure_list_get(app->lists[list], at, field).i64;
if (!into || app->nested.shared[into - 1] > 1) {
uint64_t own = nest(app, inner, into ? app->nested.list[into - 1] : NULL);
@@ -4193,7 +4388,7 @@ static void *frame_room(PureApp *app, size_t bytes, unsigned *block, size_t *use
/* An empty list of an event's list state for a call: one a call let go, or a new one (NULL: out of memory). */
static PureList *spare_list(PureApp *app, uint32_t state) {
__typeof__(app->spare[0]) *spare = &app->spare[state];
- return spare->count ? spare->lists[--spare->count] : new_list(app->module, app->module->state_ref[state]);
+ return spare->count ? spare->lists[--spare->count] : new_list(app->module, app->module->state_ref[state], false);
}
/* A call's list let go: emptied (what it held no longer counted) and kept for the next calls, unless what it keeps
@@ -4469,10 +4664,12 @@ static PureDraft *text_copy(const PureDraft *from) {
return copy;
}
-PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record) {
+/* A list of record's items: one kept past an event holds a list field's value in its stored form, an event's the number
+ * of one of the event's lists (027's 2b and 3). */
+PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record, bool kept) {
const PureRecord *r = &m->record[record];
uint8_t kinds[PURE_RECORD_FIELDS];
- for (unsigned f = 0; f < r->count; ++f) kinds[f] = pure_record_kind(m, r->first + f);
+ for (unsigned f = 0; f < r->count; ++f) kinds[f] = cell_kind(m, r->first + f, kept);
return pure_list_new(r->count, kinds);
}
@@ -5147,7 +5344,7 @@ static bool own_lists(PureApp *app, uint32_t lists) {
for (uint32_t left = lists; app->reading && left; left &= left - 1) {
unsigned i = (unsigned)__builtin_ctz(left);
if (!app->own[i]) {
- if (!(app->own[i] = new_list(m, m->state_ref[i]))) return false;
+ if (!(app->own[i] = new_list(m, m->state_ref[i], false))) return false;
(void)pure_list_keyed(app->own[i], m->state_key[i] ? (int)m->state_key[i] - 1 : -1);
if (m->state_key[i]) (void)pure_list_hashed(app->own[i]);
(void)pure_list_arrays(app->own[i]); /* in arrays, not a tree, while it can (021) */
@@ -5164,6 +5361,7 @@ static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) {
app->view_arena.used = 0;
Event ev = {.app = app, .arena = &app->view_arena, .row = row};
if (!argue(app, view, &ev)) return PURE_INVALID;
+ if (app->nested.count) unbind(app), drop_nested(app); /* a kept list's lists another view read in (027's stage 3) */
if (m->view_body[view]) { /* a body computes: its locals from 0, its lists its own, its additions counted */
memset(locals, 0, sizeof(locals));
ev.locals = locals;
@@ -5466,60 +5664,182 @@ void pure_app_read(PureApp *reader, const PureSnapshot *snapshot) {
uint8_t pure_record_kind(const PureModule *m, uint32_t field) {
static const uint8_t kinds[] = {[PURE_RECORD_NUMBER] = PURE_CELL_NUMBER, [PURE_RECORD_TEXT] = PURE_CELL_TEXT,
[PURE_RECORD_BOOL] = PURE_CELL_BOOL, [PURE_RECORD_I64] = PURE_CELL_INTEGER,
- [PURE_RECORD_LIST] = PURE_CELL_INTEGER}; /* a list in a record: its number */
+ [PURE_RECORD_LIST] = PURE_CELL_LIST};
return kinds[m->record_field_type[field]];
}
-/* A number, a bool and an i64 carry over each other (when the value fits); texts only to texts. */
+/* A field's kind in a list (kept: past an event; else an event's, which holds one of the event's lists' numbers). */
+static uint8_t cell_kind(const PureModule *m, uint32_t field, bool kept) {
+ uint8_t kind = pure_record_kind(m, field);
+ return kind == PURE_CELL_LIST && !kept ? PURE_CELL_INTEGER : kind;
+}
+
+PURE_COLD void pure_layout_of(const PureModule *m, uint32_t record, PureLayout *out) {
+ out->records = 1;
+ out->module_record[0] = (uint16_t)record;
+ for (unsigned at = 0; at < out->records; ++at) { /* breadth first: each record once, numbered as first met */
+ const PureRecord *r = &m->record[out->module_record[at]];
+ out->record[at].count = r->count;
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint32_t field = r->first + f;
+ out->record[at].kinds[f] = pure_record_kind(m, field);
+ snprintf(out->record[at].names[f], PURE_MODULE_NAME, "%s", m->record_field_names[field]);
+ out->record[at].items[f] = 0;
+ if (m->record_field_type[field] != PURE_RECORD_LIST) continue;
+ uint32_t items = nested_record(m, field);
+ unsigned k = 0;
+ while (k < out->records && out->module_record[k] != items) ++k;
+ if (k == out->records) out->module_record[out->records++] = (uint16_t)items;
+ out->record[at].items[f] = (uint8_t)k;
+ }
+ }
+}
+
+PURE_COLD bool pure_layout_same(const PureLayout *a, const PureLayout *b) {
+ if (a->records != b->records) return false;
+ for (unsigned at = 0; at < a->records; ++at) {
+ if (a->record[at].count != b->record[at].count) return false;
+ for (unsigned f = 0; f < a->record[at].count; ++f)
+ if (a->record[at].kinds[f] != b->record[at].kinds[f] || strcmp(a->record[at].names[f], b->record[at].names[f]) ||
+ (a->record[at].kinds[f] == PURE_CELL_LIST && a->record[at].items[f] != b->record[at].items[f]))
+ return false;
+ }
+ return true;
+}
+
+/* A number, a bool and an i64 carry over each other (when the value fits); texts only to texts, lists to lists. */
static bool kinds_carry(uint8_t from, uint8_t to) {
return from == to || (!pure_cell_text(from) && !pure_cell_text(to));
}
+/* Bytes made for a list value's stored form as it moves (a growing buffer). */
+typedef struct { unsigned char *bytes; size_t used, capacity; bool full; } Grown;
+static unsigned char *grown_room(Grown *g, size_t more) {
+ if (g->full) return NULL;
+ if (more > g->capacity - g->used) {
+ size_t capacity = g->capacity ? g->capacity : 256;
+ while (capacity - g->used < more) capacity *= 2;
+ unsigned char *bytes = realloc(g->bytes, capacity);
+ if (!bytes) { g->full = true; return NULL; }
+ g->bytes = bytes, g->capacity = capacity;
+ }
+ g->used += more;
+ return g->bytes + g->used - more;
+}
+
+/* For field f of record to's (in m), the field of from's record that gives it (the same name, a kind that carries
+ * over), or -1. */
+static int source_field(const PureLayout *from, unsigned at, const PureModule *m, uint32_t field) {
+ for (unsigned k = 0; k < from->record[at].count; ++k)
+ if (!strcmp(from->record[at].names[k], m->record_field_names[field]) &&
+ kinds_carry(from->record[at].kinds[k], pure_record_kind(m, field)))
+ return (int)k;
+ return -1;
+}
+
+/* A field's value, the source's or its default, as the stored form writes it (a list's moved itself). */
+static bool moved_cell(const PureLayout *from, unsigned at, int k, PureCell given, const PureModule *m, uint32_t field,
+ Grown *out);
+
+/* A list value's stored form (bytes, of from's record at) moved to record's of m by field name, item by item, into
+ * out; false on bytes not of that form (or out of memory: out->full). */
+static bool value_migrate(const PureLayout *from, unsigned at, const PureModule *m, uint32_t record, const unsigned char *bytes,
+ size_t size, Grown *out) {
+ const PureRecord *r = &m->record[record];
+ for (size_t k = 0; k < size;) {
+ PureCell cells[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < from->record[at].count; ++f) { /* the item as from's record has it */
+ if (pure_cell_text(from->record[at].kinds[f])) {
+ if (size - k < 4 || read32(bytes + k) > size - k - 4) return false;
+ cells[f] = (PureCell){.text = (unsigned char *)bytes + k + 4, .size = read32(bytes + k)};
+ k += 4 + cells[f].size;
+ } else {
+ if (size - k < 8) return false;
+ cells[f] = (PureCell){.i64 = read_i64(bytes + k)};
+ k += 8;
+ }
+ }
+ for (unsigned f = 0; f < r->count; ++f) {
+ int source = source_field(from, at, m, r->first + f);
+ if (!moved_cell(from, at, source, source >= 0 ? cells[source] : (PureCell){0}, m, r->first + f, out)) return false;
+ }
+ }
+ return !out->full;
+}
+
+static bool moved_cell(const PureLayout *from, unsigned at, int k, PureCell given, const PureModule *m, uint32_t field,
+ Grown *out) {
+ uint8_t kind = pure_record_kind(m, field);
+ if (kind == PURE_CELL_LIST) { /* the list moved item by item, or none */
+ unsigned char *length = grown_room(out, 4);
+ size_t was = out->used;
+ if (!length) return false;
+ if (k >= 0 && !value_migrate(from, from->record[at].items[k], m, nested_record(m, field), given.text, given.size, out))
+ return false;
+ write32(out->bytes + was - 4, (uint32_t)(out->used - was));
+ return true;
+ }
+ if (k < 0 || !pure_cell_as(from->record[at].kinds[k], kind, &given)) { /* its default */
+ uint32_t constant = (uint32_t)m->record_field_default[field].num;
+ given = kind == PURE_CELL_TEXT ? (PureCell){.text = (unsigned char *)m->constant_data + m->constant_offset[constant],
+ .size = m->constant_length[constant]}
+ : (PureCell){.i64 = m->record_field_default[field].i64};
+ }
+ unsigned char *put = grown_room(out, pure_cell_text(kind) ? 4 + given.size : 8);
+ if (!put) return false;
+ if (!pure_cell_text(kind)) write_i64(put, given.i64);
+ else {
+ write32(put, (uint32_t)given.size);
+ if (given.size) memcpy(put + 4, given.text, given.size);
+ }
+ return true;
+}
+
/* A list with record's fields: each takes the field of `from` with the same
- * name and a kind it carries over from, or its default. */
-PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names,
- const uint8_t *kinds, unsigned count, const PureModule *m, uint32_t record) {
+ * name and a kind it carries over from, or its default; a list field's value
+ * moves item by item (027's stage 3). */
+PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, const PureLayout *from_layout, const PureModule *m,
+ uint32_t record, bool kept) {
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]) && kinds_carry(kinds[k], pure_record_kind(m, r->first + f)))
- source[f] = (int)k;
- }
- if (!(*out = new_list(m, record))) return PURE_ALLOCATION;
+ for (unsigned f = 0; f < r->count; ++f) source[f] = source_field(from_layout, 0, m, r->first + f);
+ if (!(*out = new_list(m, record, kept))) return PURE_ALLOCATION;
PureResult result = PURE_OK;
+ Grown moved[PURE_RECORD_FIELDS] = {{0}};
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].num;
PureCell given = source[f] >= 0 ? pure_list_get(from, at, (unsigned)source[f]) : (PureCell){0};
- if (source[f] >= 0 && pure_cell_as(kinds[source[f]], pure_record_kind(m, field), &given)) cells[f] = given;
+ if (m->record_field_type[field] == PURE_RECORD_LIST && kept) { /* its items moved, or none */
+ moved[f].used = 0;
+ if (source[f] >= 0 && !value_migrate(from_layout, from_layout->record[0].items[source[f]], m, nested_record(m, field),
+ given.text, given.size, &moved[f]))
+ result = moved[f].full ? PURE_ALLOCATION : PURE_STORE_ERROR;
+ cells[f] = (PureCell){.text = moved[f].bytes, .size = moved[f].used};
+ } else if (m->record_field_type[field] == PURE_RECORD_LIST) cells[f] = (PureCell){.i64 = 0}; /* an event's: none */
+ else if (source[f] >= 0 && pure_cell_as(from_layout->record[0].kinds[source[f]], pure_record_kind(m, field), &given))
+ cells[f] = given;
else if (m->record_field_type[field] == PURE_RECORD_TEXT)
cells[f] = (PureCell){.text = (unsigned char *)m->constant_data + m->constant_offset[constant],
.size = m->constant_length[constant]};
else cells[f] = (PureCell){.i64 = m->record_field_default[field].i64}; /* a number's, a bool's or an i64's bits */
}
- result = pure_list_insert(*out, at, cells, NULL);
+ if (result == PURE_OK) result = pure_list_insert(*out, at, cells, NULL);
}
+ for (unsigned f = 0; f < r->count; ++f) free(moved[f].bytes);
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. */
+/* A kept list keeps its items; with the same record (laid out the same) 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];
- uint8_t kinds[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];
- kinds[k] = pure_record_kind(old, o->first + k);
- same = same && !strcmp(names[k], m->record_field_names[r->first + k]) && kinds[k] == pure_record_kind(m, r->first + k);
- }
- if (same) { *out = from; return PURE_OK; }
- return pure_list_migrate(out, from, names, kinds, o->count, m, record);
+ const PureModule *m, uint32_t record, bool kept) {
+ PureLayout was, now;
+ pure_layout_of(old, old_record, &was);
+ pure_layout_of(m, record, &now);
+ if (pure_layout_same(&was, &now)) { *out = from; return PURE_OK; }
+ return pure_list_migrate(out, from, &was, m, record, kept);
}
/* Open a module in an app. States keep their values by name and kind (texts
@@ -5681,7 +6001,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
bool saving = lists_stored && !rows && !transient;
if (kept[i] >= 0) {
result = keep_list(&lists[i], app->lists[kept[i]], old, old->state_ref[kept[i]], module,
- module->state_ref[i]);
+ module->state_ref[i], !rows);
list_drafts[i] = app->list_drafts[kept[i]];
if (!transient && !was_transient)
listed[i] = (PureSaved){app->image_seq[kept[i]], app->logged_since[kept[i]], app->logged[kept[i]], false};
@@ -5690,7 +6010,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
} else if (saving) {
result = pure_save_open_list(app, module, i, &lists[i], &list_drafts[i], &listed[i]);
rewrite[i] = result == PURE_OK && listed[i].rewrite;
- } else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION;
+ } else if (!(lists[i] = new_list(module, module->state_ref[i], !rows))) result = PURE_ALLOCATION;
} else if (kept[i] >= 0 && valued) {
PureCell cell = {.i64 = app->values[kept[i]].i64};
values[i] = pure_cell_as(pure_value_kind(was), pure_value_kind(now), &cell) ? (PureValue){.i64 = cell.i64}