diff options
| -rw-r--r-- | cli/pure-run.c | 59 | ||||
| -rw-r--r-- | core/module-list.h | 11 | ||||
| -rw-r--r-- | core/module-save.c | 93 | ||||
| -rw-r--r-- | core/module-save.h | 29 | ||||
| -rw-r--r-- | core/module.c | 428 | ||||
| -rw-r--r-- | core/module.h | 19 | ||||
| -rw-r--r-- | core/test-module.c | 131 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 16 |
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`로 멈춘다. 조용히 버리지 않는다. - **저장은 묶음이다.** 저장 한 번이 마지막 저장 이후 메시지가 고친 문서들과 바뀐 상태 문서들을 저장소의 |
