aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cli/pure-run.c59
-rw-r--r--core/module-list.h11
-rw-r--r--core/module-save.c93
-rw-r--r--core/module-save.h29
-rw-r--r--core/module.c428
-rw-r--r--core/module.h19
-rw-r--r--core/test-module.c131
-rw-r--r--docs/design/008-module-format.md16
8 files changed, 677 insertions, 109 deletions
diff --git a/cli/pure-run.c b/cli/pure-run.c
index 97bd6d5..dceb841 100644
--- a/cli/pure-run.c
+++ b/cli/pure-run.c
@@ -49,22 +49,56 @@ static bool collect(void *context, PureSpan span) {
return true;
}
-static void show_list(const PureModule *m, const PureList *list, const PureRecord *record) {
+static uint32_t le32(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 show_cell(const PureModule *m, uint32_t field, PureCell cell, bool kept);
+
+/* A list kept past an event holds a list field's value in its items' stored form (Pure 027's stage 3): its items. */
+static void show_value_items(const PureModule *m, uint32_t record, const unsigned char *bytes, size_t size) {
+ const PureRecord *r = &m->record[record];
+ putchar('[');
+ for (size_t at = 0, item = 0; at < size; ++item) {
+ printf("%s{", item ? "," : "");
+ for (unsigned f = 0; f < r->count && at <= size; ++f) {
+ uint8_t type = m->record_field_type[r->first + f];
+ PureCell cell = {0};
+ if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) {
+ cell.size = size - at >= 4 ? le32(bytes + at) : 0;
+ cell.text = (unsigned char *)bytes + at + 4;
+ at += 4 + cell.size;
+ } else {
+ memcpy(&cell.i64, bytes + at, 8);
+ at += 8;
+ }
+ printf("%s\"%s\":", f ? "," : "", m->record_field_names[r->first + f]);
+ show_cell(m, r->first + f, cell, true);
+ }
+ putchar('}');
+ }
+ putchar(']');
+}
+
+static void show_cell(const PureModule *m, uint32_t field, PureCell cell, bool kept) {
+ uint8_t type = m->record_field_type[field];
+ if (type == PURE_RECORD_BOOL) printf("%s", cell.num ? "true" : "false");
+ else if (type == PURE_RECORD_I64) printf("%" PRId64, cell.i64);
+ else if (type == PURE_RECORD_NUMBER) printf("%.17g", cell.num);
+ else if (type == PURE_RECORD_LIST && kept) show_value_items(m, (uint32_t)m->record_field_default[field].i64, cell.text, cell.size);
+ else if (type == PURE_RECORD_LIST) printf("%" PRId64, cell.i64); /* an event's list: its number among the event's */
+ else {
+ putchar('"');
+ json_span(NULL, (PureSpan){cell.text, cell.size});
+ putchar('"');
+ }
+}
+
+static void show_list(const PureModule *m, const PureList *list, const PureRecord *record, bool kept) {
putchar('[');
for (size_t at = 0; at < pure_list_count(list); ++at) {
printf("%s{", at ? "," : "");
for (unsigned f = 0; f < record->count; ++f) {
- PureCell cell = pure_list_get(list, at, f);
printf("%s\"%s\":", f ? "," : "", m->record_field_names[record->first + f]);
- uint8_t type = m->record_field_type[record->first + f];
- if (type == PURE_RECORD_BOOL) printf("%s", cell.num ? "true" : "false");
- else if (type == PURE_RECORD_I64) printf("%" PRId64, cell.i64);
- else if (type == PURE_RECORD_NUMBER) printf("%.17g", cell.num);
- else {
- putchar('"');
- json_span(NULL, (PureSpan){cell.text, cell.size});
- putchar('"');
- }
+ show_cell(m, record->first + f, pure_list_get(list, at, f), kept);
}
putchar('}');
}
@@ -106,7 +140,8 @@ static void show(PureApp *app, const char *failure) {
putchar('"');
pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), json_span, NULL);
putchar('"');
- } else if (m->state_type[i] == PURE_STATE_LIST) show_list(m, app->lists[i], &m->record[m->state_ref[i]]);
+ } else if (m->state_type[i] == PURE_STATE_LIST)
+ show_list(m, app->lists[i], &m->record[m->state_ref[i]], !(m->state_flags[i] & PURE_LIST_ROWS >> 8));
else if (m->state_type[i] == PURE_STATE_BOOL) printf("%s", app->values[i].num ? "true" : "false");
else if (m->state_type[i] == PURE_STATE_I64) printf("%" PRId64, app->values[i].i64);
else printf("%.17g", app->values[i].num);
diff --git a/core/module-list.h b/core/module-list.h
index 97a6ee1..d1f27b2 100644
--- a/core/module-list.h
+++ b/core/module-list.h
@@ -51,11 +51,12 @@ typedef struct {
enum { PURE_LIST_ITEMS = 1 << 24, PURE_LIST_RUN = 32 };
/* A field's kind: a number, a text (UTF-8, checked when read), bytes (any: a
- * runtime list's), a bool (a number 0 or 1, kept as a number is) or an
- * integer (an int64_t, ordered as one). A stored list's header names its
- * fields by these kinds. */
-enum { PURE_CELL_NUMBER = 0, PURE_CELL_TEXT = 1, PURE_CELL_BYTES = 2, PURE_CELL_BOOL = 3, PURE_CELL_INTEGER = 4 };
-static inline bool pure_cell_text(uint8_t kind) { return kind == PURE_CELL_TEXT || kind == PURE_CELL_BYTES; }
+ * runtime list's), a bool (a number 0 or 1, kept as a number is), an
+ * integer (an int64_t, ordered as one) or a list (027's stage 3: a list
+ * value in a list kept past an event, in its items' stored form, held as
+ * bytes are). A stored list's header names its fields by these kinds. */
+enum { PURE_CELL_NUMBER = 0, PURE_CELL_TEXT = 1, PURE_CELL_BYTES = 2, PURE_CELL_BOOL = 3, PURE_CELL_INTEGER = 4, PURE_CELL_LIST = 5 };
+static inline bool pure_cell_text(uint8_t kind) { return kind == PURE_CELL_TEXT || kind == PURE_CELL_BYTES || kind == PURE_CELL_LIST; }
/* Whether a number, bool or integer cell holds what its kind may: a finite
* number, a bool 0 or 1 (either zero), any integer. */
bool pure_cell_fits(uint8_t kind, PureCell cell);
diff --git a/core/module-save.c b/core/module-save.c
index 7d9d951..2b3bd73 100644
--- a/core/module-save.c
+++ b/core/module-save.c
@@ -157,7 +157,7 @@ PURE_COLD PureResult pure_save_open_text(PureApp *app, const PureModule *m, uint
/* ---- Lists: images and the log */
enum { OUTPUTS_NAMED = 1 + PURE_MODULE_INPUTS * (2 + PURE_MODULE_NAME) + PURE_MODULE_FIELDS * (2 + PURE_MODULE_NAME),
- IMAGE_HEADER = 5 + PURE_RECORD_FIELDS * (2 + PURE_MODULE_NAME) + 8 + OUTPUTS_NAMED };
+ IMAGE_HEADER = 6 + PURE_MODULE_RECORDS * (1 + PURE_RECORD_FIELDS * (3 + PURE_MODULE_NAME)) + 8 + OUTPUTS_NAMED };
static size_t header_of(unsigned count, const uint8_t *text, const char (*names)[PURE_MODULE_NAME], uint64_t seq,
unsigned char *out) {
@@ -174,11 +174,62 @@ static size_t header_of(unsigned count, const uint8_t *text, const char (*names)
return (size_t)(put_le(out + at, seq, 8) - out);
}
+/* A list record's fields as header_of writes them, a list field's with the number of its items' record after its name,
+ * and, once any field is a list's (027's stage 3), the graph's other records after them: a u8 count, each record a u8
+ * count of fields and its fields so (PureLayout). */
+static unsigned char *layout_fields(const PureLayout *l, unsigned at, unsigned char *out) {
+ for (unsigned f = 0; f < l->record[at].count; ++f) {
+ size_t length = strlen(l->record[at].names[f]);
+ *out++ = l->record[at].kinds[f];
+ *out++ = (unsigned char)length;
+ memcpy(out, l->record[at].names[f], length);
+ out += length;
+ if (l->record[at].kinds[f] == PURE_CELL_LIST) *out++ = l->record[at].items[f];
+ }
+ return out;
+}
+
static size_t image_header(const PureModule *m, uint32_t record, uint64_t seq, unsigned char *out) {
- 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);
- return header_of(r->count, kinds, m->record_field_names + r->first, seq, out);
+ PureLayout l;
+ pure_layout_of(m, record, &l);
+ memcpy(out, image_magic, 4);
+ out[4] = (unsigned char)l.record[0].count;
+ unsigned char *at = layout_fields(&l, 0, out + 5);
+ bool lists = m->record_nested[record];
+ if (lists) *at++ = (unsigned char)(l.records - 1);
+ for (unsigned k = 1; lists && k < l.records; ++k) {
+ *at++ = (unsigned char)l.record[k].count;
+ at = layout_fields(&l, k, at);
+ }
+ return (size_t)(put_le(at, seq, 8) - out);
+}
+
+/* Past a record's count fields as layout_fields wrote them (from at; read_fields checked them): where they end. */
+static size_t skip_fields(const unsigned char *bytes, size_t size, size_t at, unsigned count) {
+ for (unsigned f = 0; f < count && at + 2 <= size; ++f) {
+ uint8_t kind = bytes[at];
+ at += 2 + bytes[at + 1] + (kind == PURE_CELL_LIST);
+ }
+ return at <= size ? at : 0;
+}
+
+/* A record's fields as layout_fields wrote them (at, moved past them), into l's record k: false when not well formed
+ * (no field twice, kinds a record's, a list's items' record among the graph's: below records). */
+static bool read_fields(const unsigned char *bytes, size_t size, size_t *at, unsigned count, PureLayout *l, unsigned k,
+ unsigned records, bool *lists) {
+ l->record[k].count = count;
+ for (unsigned f = 0; f < count; ++f) {
+ if (!named(bytes, size, at, 0, &l->record[k].kinds[f], l->record[k].names[f])) return false;
+ uint8_t kind = l->record[k].kinds[f];
+ if (kind > PURE_CELL_LIST || kind == PURE_CELL_BYTES) return false;
+ for (unsigned g = 0; g < f; ++g) if (!strcmp(l->record[k].names[g], l->record[k].names[f])) return false;
+ l->record[k].items[f] = 0;
+ if (kind != PURE_CELL_LIST) continue;
+ if (*at >= size || bytes[*at] >= records) return false;
+ l->record[k].items[f] = bytes[(*at)++];
+ *lists = true;
+ }
+ return true;
}
PureList *pure_runtime_list_new(uint32_t place) {
@@ -446,21 +497,27 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
if (result == PURE_OK && pure_draft_revision(*legacy) && pure_draft_size(*legacy))
result = read_whole(*legacy, &bytes, &size);
}
- /* The stored record: its fields' kinds (PURE_CELL_...: a record's, no runtime bytes) and names. */
- char names[PURE_RECORD_FIELDS][PURE_MODULE_NAME] = {{0}};
- uint8_t kinds[PURE_RECORD_FIELDS] = {0};
+ /* The stored record graph: its fields' kinds (PURE_CELL_...: a record's, no runtime bytes) and names, and the
+ * records of the lists in its fields (an image's alone, 027's stage 3). */
+ PureLayout stored_layout = {.records = 1};
unsigned count = 0;
size_t at = 0, items = 0;
uint64_t holds = 0;
bool stored = result == PURE_OK && bytes, first = stored && !image && size >= 4 && !memcmp(bytes, list_magic_1, 4);
if (stored) {
const unsigned char *magic = image ? image_magic : first ? list_magic_1 : list_magic;
- bool ok = size >= 5 && !memcmp(bytes, magic, 4) && bytes[4] <= PURE_RECORD_FIELDS;
+ bool ok = size >= 5 && !memcmp(bytes, magic, 4) && bytes[4] <= PURE_RECORD_FIELDS, lists = false;
count = ok ? bytes[4] : 0;
at = 5;
- for (unsigned f = 0; ok && f < count; ++f) {
- ok = named(bytes, size, &at, 0, &kinds[f], names[f]) && kinds[f] <= PURE_CELL_INTEGER && kinds[f] != PURE_CELL_BYTES;
- for (unsigned k = 0; k < f; ++k) ok = ok && strcmp(names[k], names[f]);
+ ok = ok && read_fields(bytes, size, &at, count, &stored_layout, 0, 256, &lists) && (image || !lists);
+ if (ok && lists) { /* the graph's other records */
+ ok = at < size && bytes[at] < PURE_MODULE_RECORDS;
+ stored_layout.records = ok ? 1u + bytes[at++] : 1;
+ for (unsigned k = 1; ok && k < stored_layout.records; ++k)
+ ok = at < size && bytes[at] <= PURE_RECORD_FIELDS &&
+ read_fields(bytes, size, &(size_t){at + 1}, bytes[at], &stored_layout, k, stored_layout.records, &lists) &&
+ (at = skip_fields(bytes, size, at + 1, bytes[at])) != 0;
+ for (unsigned f = 0; ok && f < count; ++f) ok = stored_layout.record[0].items[f] < stored_layout.records;
}
if (ok && image) { /* the save it holds, then its items */
ok = size - at >= 8;
@@ -475,7 +532,7 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
}
PureList *from = NULL;
if (result == PURE_OK && stored) {
- from = pure_list_new(count, kinds);
+ from = pure_list_new(count, stored_layout.record[0].kinds);
result = from ? pure_list_read(from, bytes + at, items) : PURE_ALLOCATION;
if (result == PURE_OK && !image) result = pure_list_replay(from, bytes + at + items, size - at - items);
}
@@ -492,9 +549,9 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
saved->logged += change->records.size;
}
}
- bool same = stored && count == r->count;
- for (unsigned f = 0; same && f < count; ++f)
- same = kinds[f] == pure_record_kind(m, r->first + f) && !strcmp(names[f], m->record_field_names[r->first + f]);
+ PureLayout layout;
+ pure_layout_of(m, record, &layout);
+ bool same = stored && pure_layout_same(&stored_layout, &layout);
if (result == PURE_OK && same && image) {
*out = from;
saved->image = holds;
@@ -503,12 +560,10 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
if (result == PURE_OK) {
/* New, stored before images, or stored with another record (its items
* move by field name): its image is written before anything else of it. */
- const char *field_names[PURE_RECORD_FIELDS];
- for (unsigned f = 0; f < count; ++f) field_names[f] = names[f];
if (same) {
*out = from;
from = NULL;
- } else if (from) result = pure_list_migrate(out, from, field_names, kinds, count, m, record);
+ } else if (from) result = pure_list_migrate(out, from, &stored_layout, m, record, true);
else {
uint8_t own[PURE_RECORD_FIELDS];
for (unsigned f = 0; f < r->count; ++f) own[f] = pure_record_kind(m, r->first + f);
diff --git a/core/module-save.h b/core/module-save.h
index 5c81504..281854b 100644
--- a/core/module-save.h
+++ b/core/module-save.h
@@ -74,12 +74,27 @@ uint32_t pure_fields_lay_out(PureField *fields, uint32_t count);
PureResult pure_send_recast(const PureField *from, uint32_t from_count, const PureField *to, uint32_t to_count,
PureSpan payload, unsigned char **out, size_t *size);
-/* A record field's kind in a list, as a stored list names it too (PURE_CELL_...). */
+/* A record field's kind in a list kept past an event, as a stored list names it too (PURE_CELL_...: a list field's
+ * PURE_CELL_LIST; an event's list holds one of the event's lists' numbers there instead, an integer). */
uint8_t pure_record_kind(const PureModule *, uint32_t field);
-/* A list with record's fields, taking from `from` (its fields' names and
- * kinds) each field of the same name and kind, or a number, a bool and an
- * i64 from each other when the value fits (pure_cell_as), and giving the
- * others their defaults (module.c). */
-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);
+/* A record graph as a list kept past an event names it (027's stage 3): its records, from the list's own (0) on in the
+ * order its list fields first meet them, each its fields' kinds (PURE_CELL_...) and names, and a list field's items'
+ * record by its number here. A module's graph knows each record's number in the module too. */
+typedef struct {
+ unsigned records;
+ struct {
+ unsigned count;
+ uint8_t kinds[PURE_RECORD_FIELDS], items[PURE_RECORD_FIELDS];
+ char names[PURE_RECORD_FIELDS][PURE_MODULE_NAME];
+ } record[PURE_MODULE_RECORDS];
+ uint16_t module_record[PURE_MODULE_RECORDS];
+} PureLayout;
+void pure_layout_of(const PureModule *, uint32_t record, PureLayout *out);
+bool pure_layout_same(const PureLayout *, const PureLayout *);
+/* A list with record's fields, taking from `from` (laid out as from_layout's record 0) each field of the same name and
+ * kind, or a number, a bool and an i64 from each other when the value fits (pure_cell_as), and giving the others their
+ * defaults (module.c); a list field's values move so too, item by item (kept: a list kept past an event, holding
+ * them in their stored form, else an event's, holding none yet). */
+PureResult pure_list_migrate(PureList **out, const PureList *from, const PureLayout *from_layout, const PureModule *m,
+ uint32_t record, bool kept);
#endif
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}
diff --git a/core/module.h b/core/module.h
index 18947d0..15109b6 100644
--- a/core/module.h
+++ b/core/module.h
@@ -93,13 +93,18 @@ typedef enum { PURE_BITS_AND, PURE_BITS_OR, PURE_BITS_XOR, PURE_BITS_LEFT, PURE_
* number (else LIMIT), in its base with a sign; FIXED takes a number. */
enum { PURE_MODULE_HELD = 32, PURE_HELD_TEXT = 16, PURE_HELD_APPEND = 1 << 8 };
/* 027's stage 2b: lists in records. A record field of type LIST holds a list
- * of the record its default names (an event's alone: an event's lists, and
- * the lists in their items, are the only ones that may hold one). The value
- * is the list's number among the event's lists made so (0: an empty one),
- * an i64 in its cell, so moves and reads (ITEM_GET) are an integer's; such
- * lists go as the event or a view's body ends. A list is a value: storing
- * one in a second cell shares it, and a list shared or bound is copied
- * before it changes (so no other value sees the change).
+ * of the record its default names. In an event's list (and the lists in its
+ * items) the value is the list's number among the event's lists made so (0:
+ * an empty one), an i64 in its cell, so moves and reads (ITEM_GET) are an
+ * integer's; such lists go as the event or a view's body ends. A list is a
+ * value: storing one in a second cell shares it, and a list shared or bound
+ * is copied before it changes (so no other value sees the change). Since
+ * stage 3 a stored or a transient list's records may hold lists too (a
+ * message's rows not, nor as a key): there the cell holds the value in its
+ * items' stored form (PURE_CELL_LIST), read into one of the event's lists
+ * as ITEM_GET reads it and written from one as it is set (LIST_APPEND,
+ * LIST_INSERT, ITEM_SET; APPEND_IN reads, appends and writes back), so
+ * saving, undoing and sharing with readers hold it as a text's bytes.
* LIST_VALUE pushes the value of an event's list state (the argument): a
* copy of its items. LIST_BIND gives the bound list state (a register: an
* event's list flagged PURE_LIST_BOUND, read and walked, never changed by
diff --git a/core/test-module.c b/core/test-module.c
index 893aeab..eb6752d 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -6337,8 +6337,8 @@ static void lists_in_records(void) {
static const O nothing[] = {{C, 0, 0}, {ST, 0, 0}};
H two = {1, 0, 2};
Spec kept = {nums, 1, inputs, 1, NULL, 0, &two, 1, nothing, 2, NULL, 0, stored, 1, NULL, 0, NULL, 0, records, 2, NULL, 0};
- CHECK(load(kept, &module_b) != PURE_OK);
- printf("{\"lists_in_records\":{\"rounds\":3,\"refused\":%zu}}\n", LEN(refused) + 1);
+ CHECK(load(kept, &module_b) == PURE_OK); /* a stored list's records hold lists too since 027's stage 3 */
+ printf("{\"lists_in_records\":{\"rounds\":3,\"refused\":%zu}}\n", LEN(refused));
}
/* Functions and characters (027's 2c): a function calls itself; each call has its own locals, lists, registers and
@@ -6503,6 +6503,131 @@ static void functions_and_characters(void) {
printf("{\"functions_and_characters\":{\"rounds\":3,\"refused\":%zu}}\n", LEN(refused) + 3);
}
+/* 027's stage 3: an event changes a number and the list in a stored list's item's field (APPEND_IN: read in, appended,
+ * written back); the process dies at each commit stage, or the commit fails. Starting again finds both as before or
+ * both as after. */
+enum { NESTED_BOARDS = 1, NESTED_NONE = 2 };
+static unsigned nested_consistent(const PureApp *app) {
+ unsigned n = (unsigned)app->values[0].num;
+ CHECK(app->values[0].num == n && pure_list_count(app->lists[NESTED_BOARDS]) == (n > 0));
+ if (!n) return 0;
+ PureCell items = pure_list_get(app->lists[NESTED_BOARDS], 0, 1); /* the leaves' stored form: an i64 each */
+ CHECK(items.size == 8u * n);
+ for (unsigned k = 0; k < n; ++k) {
+ int64_t v;
+ memcpy(&v, items.text + 8 * k, 8);
+ CHECK(v == (int64_t)k + 1);
+ }
+ return n;
+}
+
+static void nested_crashes(void) {
+ enum { LC_ = PURE_OP_LIST_COUNT, LV_ = PURE_OP_LIST_VALUE, AIN_ = PURE_OP_APPEND_IN, EQ_ = PURE_OP_EQ };
+ static const S nums[] = {{"n", 0}};
+ static const T states[] = {{"boards", 3, 1}, {"let.none", 3 | (uint32_t)PURE_LIST_ROWS << 8, 0}};
+ static const RF leaf_fields[] = {{"v", 3, 0}};
+ static const RF board_fields[] = {{"id", 3, 0}, {"items", 4, 0}};
+ static const R records[] = {{"Leaf", leaf_fields, 1}, {"Board", board_fields, 2}};
+ static const I inputs[] = {KEY("go", "KeyG")};
+ const O code[] = {
+ {LC_, NESTED_BOARDS, 0}, {C, 0, 0}, {EQ_, 0, 0}, {JZ, 7, 0}, IC(0), {LV_, NESTED_NONE, 0}, {LA, NESTED_BOARDS, 0},
+ {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* n = n + 1 */
+ {XF, NESTED_BOARDS, 0}, {L, 0, 0}, {PURE_OP_I64_FROM, 0, 0}, {AIN_, ITEM(NESTED_BOARDS, 1), 0}, /* items += n */
+ };
+ H handler = {1, 0, LEN(code)};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, code, LEN(code), NULL, 0, states, LEN(states), NULL, 0, NULL, 0,
+ records, 2, NULL, 0};
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "nested crashes: %s\n", error); abort(); }
+ char *path = new_store();
+ 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);
+ CHECK(nested_consistent(&app) == 2);
+ pure_app_stop(&app);
+ unsigned expected = 2;
+ for (int stage = PURE_AFTER_HEADER; stage <= PURE_BEFORE_DIR_SYNC + 2; ++stage) { /* the tree's stages, then the log's */
+ int at = stage <= PURE_BEFORE_DIR_SYNC ? stage : PURE_LOG_BEFORE_WRITE + (stage - PURE_BEFORE_DIR_SYNC - 1);
+ 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 = at;
+ pure_app_input(&doomed, 0, PURE_EDGE_DOWN);
+ _exit(0);
+ }
+ int status;
+ CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77);
+ if (at == PURE_BEFORE_DIR_SYNC) ++expected; /* the head was written */
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && nested_consistent(&app) == expected);
+ pure_app_stop(&app);
+ }
+ crash = 0;
+ 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) /* a failed commit: nothing */
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_STORE_ERROR && nested_consistent(&app) == expected);
+ fail_stage = PURE_LOG_BEFORE_WRITE;
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_STORE_ERROR && nested_consistent(&app) == expected);
+ fail_stage = -1;
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && nested_consistent(&app) == expected + 1);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && nested_consistent(&app) == expected + 1);
+ pure_app_stop(&app);
+ remove_store(path);
+ printf("{\"nested_crashes\":{\"stages\":%d,\"kept\":%u}}\n", PURE_BEFORE_DIR_SYNC + 3 - PURE_AFTER_HEADER, expected + 1);
+}
+
+/* 027's stage 3: a module replaced while the app runs moves a stored and a transient list's items, and the items of the
+ * lists in their fields, by field name: a field the items' record gains takes its default, a field it moves keeps its
+ * values. */
+static void nested_replace(void) {
+ enum { LC_ = PURE_OP_LIST_COUNT, LV_ = PURE_OP_LIST_VALUE, AIN_ = PURE_OP_APPEND_IN, EQ_ = PURE_OP_EQ };
+ static const S nums[] = {{"n", 0}};
+ static const T states[] = {{"boards", 3, 1}, {"let.none", 3 | (uint32_t)PURE_LIST_ROWS << 8, 0}, {"cache", 3 | TR, 1}};
+ static const RF leaf_fields[] = {{"v", 3, 0}};
+ static const RF board_fields[] = {{"id", 3, 0}, {"items", 4, 0}};
+ static const R records[] = {{"Leaf", leaf_fields, 1}, {"Board", board_fields, 2}};
+ static const I inputs[] = {KEY("go", "KeyG")};
+ const O code[] = {
+ {LC_, 1, 0}, {C, 0, 0}, {EQ_, 0, 0}, {JZ, 10, 0}, IC(9), {LV_, 2, 0}, {LA, 1, 0}, IC(8), {LV_, 2, 0}, {LA, 3, 0},
+ {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0},
+ {XF, 1, 0}, {L, 0, 0}, {PURE_OP_I64_FROM, 0, 0}, {AIN_, ITEM(1, 1), 0}, /* boards.first.items += n */
+ {XF, 3, 0}, {L, 0, 0}, {C, 0, 10}, {MUL, 0, 0}, {PURE_OP_I64_FROM, 0, 0}, {AIN_, ITEM(3, 1), 0}, /* cache's += 10n */
+ };
+ H handler = {1, 0, LEN(code)};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, code, LEN(code), NULL, 0, states, LEN(states), NULL, 0, NULL, 0,
+ records, 2, NULL, 0};
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "nested replace: %s\n", error); abort(); }
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ for (int k = 0; k < 3; ++k) CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ /* The leaves gain w (7 by default) and the boards' fields swap places. */
+ static const RF leaf_wider[] = {{"v", 3, 0}, {"w", 0, 7}};
+ static const RF board_swapped[] = {{"items", 4, 0}, {"id", 3, 0}};
+ static const R wider[] = {{"Leaf", leaf_wider, 2}, {"Board", board_swapped, 2}};
+ static const O nothing[] = {{C, 0, 0}, {ST, 0, 0}};
+ H two = {1, 0, 2};
+ Spec next = {nums, 1, inputs, 1, NULL, 0, &two, 1, nothing, 2, NULL, 0, states, LEN(states), NULL, 0, NULL, 0, wider, 2,
+ NULL, 0};
+ CHECK(load(next, &module_b) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK);
+ for (unsigned list = 1; list <= 3; list += 2) { /* boards, then cache */
+ CHECK(pure_list_count(app.lists[list]) == 1 && pure_list_get(app.lists[list], 0, 1).i64 == (list == 1 ? 9 : 8));
+ PureCell items = pure_list_get(app.lists[list], 0, 0);
+ CHECK(items.size == 3 * 16);
+ for (unsigned k = 0; k < 3; ++k) {
+ int64_t v;
+ double w;
+ memcpy(&v, items.text + 16 * k, 8);
+ memcpy(&w, items.text + 16 * k + 8, 8);
+ CHECK(v == (int64_t)(k + 1) * (list == 1 ? 1 : 10) && w == 7);
+ }
+ }
+ pure_app_stop(&app);
+ printf("{\"nested_replace\":{\"lists\":2,\"items\":3}}\n");
+}
+
/* 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 };
@@ -6880,6 +7005,8 @@ int main(void) {
formats_and_held();
lists_in_records();
functions_and_characters();
+ nested_crashes();
+ nested_replace();
memory_budget();
work_budget();
fold_mutations();
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index 84c8e18..a23f140 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -25,13 +25,20 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계):
넷, 90–93). 2a로 **바이트 자르기, 한 바이트, 이름으로 담은 바이트와 text**(94–97), CONCAT의 바이트, NUM_TEXT의 형식이 더해졌다. 2b로
**레코드 안의 리스트**(레코드 필드의 타입 4, 98–100), 레지스터(리스트 상태의 플래그 512), 경로 이름의 레코드 필드, **수 읽기**(101)가
더해졌다. 2c로 **함수**(CALL·RETURN, 102–103, 머리말의 함수 수와 함수 표)와 **글자**(CHAR_AT·CHAR, 104–105)가 더해졌다. 판은
-그대로다(0이던 머리말 자리에 함수 수, 함수가 없는 모듈은 배치가 같다).
+그대로다(0이던 머리말 자리에 함수 수, 함수가 없는 모듈은 배치가 같다). 3단계로 **저장과 transient 리스트의 레코드도 리스트를 담는다**
+(아래 "레코드 안의 리스트"). 모듈의 형식은 바뀌지 않고, 리스트의 이미지 머리가 레코드 그래프를 적는다.
**레코드 안의 리스트**(027의 2b).
- 레코드 필드의 타입 4(LIST)는 항목들의 레코드를 기본값 자리(i64)에 둔다.
- 칸의 값은 이벤트가 만든 리스트의 번호(0은 빈 리스트)다. 정수처럼 옮겨지고 ITEM_GET으로 읽힌다. 스택의 타입은 리스트이고, 그
레코드를 가진다.
-- 그런 레코드는 이벤트의 리스트(let의 것)에만 둔다. 저장 상태, transient, 메시지의 행은 받지 않고, 리스트 필드는 키가 되지 않는다.
+- 3단계부터 저장 상태와 transient의 리스트도 그런 레코드를 받는다. 메시지의 행은 받지 않고, 리스트 필드는 키가 되지 않는다.
+- 이벤트를 넘어 사는 리스트(저장, transient)의 칸은 리스트 값을 그 항목들의 저장 형태로 담는다(칸의 종류 5, 바이트처럼). 항목마다
+ 필드의 차례로, 수·bool·i64는 8바이트 little-endian, text와 리스트는 u32le 길이와 바이트(리스트는 다시 이 형태)다.
+ - ITEM_GET은 그것을 이벤트의 리스트 하나로 읽어 그 번호를 놓는다(읽은 바이트를 이벤트의 몫에 센다).
+ - LIST_APPEND·LIST_INSERT·ITEM_SET은 이벤트의 리스트를 그 형태로 써 넣는다. APPEND_IN은 읽고, 덧붙이고, 다시 쓴다.
+ - 그래서 저장(이미지와 로그), 되돌리기, 읽는 쪽과의 나눔은 text의 바이트를 다루듯 그것을 다룬다. 바꿀 때마다 그 리스트 값
+ 전체를 쓴다(비용은 바뀐 것이 아니라 그 값의 크기에 비례한다). 구조를 나누는 표현은 뜻을 바꾸지 않는 구현의 선택으로 남는다.
- 값으로서의 리스트는 나눠 가질 수 있다. 다른 칸에 담으면 나눠 가지고, 나눠 가지거나 레지스터에 묶인 리스트는 바뀌기 전에
복사된다.
- 이벤트(그리고 보기의 본문)가 시작할 때 그 리스트들은 모두 사라진다. 담은 바이트는 이벤트의 몫에 센다.
@@ -376,7 +383,9 @@ text·리스트가 남을 때만 위치를 유지한다.
| 수·bool·커서·인덱스 | `values`: `PVS1`, 상태마다 종류·이름·binary64(bool은 0이나 1). 바뀐 이벤트에서만 다시 쓴다 |
- **리스트: 이미지와 로그.** 리스트는 저장소 디렉터리의 두 곳에 산다. **이미지** `image-이름`(파일 하나)은
- `PLI1`, 레코드 필드 수, 필드마다 종류(0 수, 1 text, 3 bool)·이름, 그것이 담은 저장의 번호(u64), 항목(수와 bool은 binary64, text는 u32 길이와
+ `PLI1`, 레코드 필드 수, 필드마다 종류(0 수, 1 text, 3 bool, 4 i64, 5 리스트)·이름(리스트는 그 뒤에 u8: 항목들의 레코드 번호), 리스트
+ 필드가 있으면 레코드 그래프의 나머지(u8 수, 레코드마다 u8 필드 수와 필드들, 번호는 그래프에서의 자리: 0이 이 리스트의 레코드,
+ 나머지는 리스트 필드가 처음 만나는 차례), 그것이 담은 저장의 번호(u64), 항목(수와 bool은 binary64, text와 리스트는 u32 길이와
바이트)이고, 통째로 옆에 써서 동기화한 뒤 이름을 바꿔 끼운다(파일 전체의 다이제스트로 잘림·손상을 거부).
**로그**(`log-0`, `log-1`, ... 구간, `core/log.h`)는 리스트를 바꾼 저장마다 항목 하나다. 항목은 저장 번호, 앞 항목의
번호와 다이제스트, 트리도 커밋했는지, 페이로드(바뀐 리스트마다 이름과 변경 기록, 모두 그 리스트 이미지의 필드로)다.
@@ -399,6 +408,7 @@ text·리스트가 남을 때만 위치를 유지한다.
- **시작.** 저장된 상태를 이름과 종류로 불러온다. 리스트는 이미지를 읽고 로그에서 그 이미지가 담은 저장 뒤의 변경을
차례로 다시 한다. 없는 상태는 초기값으로 시작한다. 레코드가 달라진 리스트는
이름·종류가 같은 필드를 옮기고(수와 bool은 교체처럼 서로 옮긴다) 새 필드는 기본값을 쓴 뒤 다시 저장한다(교체와 같은 규칙, 마이그레이션 스크립트 없음).
+ 리스트 필드의 항목도 그래프의 레코드를 따라 같은 규칙으로 옮겨진다(3단계).
커서가 text 범위나 글자 경계를 벗어나거나, 인덱스가 리스트 밖이면 0이 된다. 깨진 리스트·text(형식 오류, UTF-8
아님)는 시작을 `store_error`로 멈춘다. 조용히 버리지 않는다.
- **저장은 묶음이다.** 저장 한 번이 마지막 저장 이후 메시지가 고친 문서들과 바뀐 상태 문서들을 저장소의