aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/module-jit.c3
-rw-r--r--core/module-text.c24
-rw-r--r--core/module-text.h4
-rw-r--r--core/module.c232
-rw-r--r--core/module.h29
-rw-r--r--core/test-module.c76
6 files changed, 349 insertions, 19 deletions
diff --git a/core/module-jit.c b/core/module-jit.c
index 80a242e..0254430 100644
--- a/core/module-jit.c
+++ b/core/module-jit.c
@@ -1245,7 +1245,8 @@ static bool event_list(const PureModule *m, uint32_t list) {
static bool numbers_only(const PureModule *m, uint32_t list) {
const PureRecord *record = &m->record[m->state_ref[list]];
for (unsigned f = 0; f < record->count; ++f)
- if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT) return false;
+ if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT || m->record_field_type[record->first + f] == PURE_RECORD_LIST)
+ return false; /* a list in a record is shared by count (module.c's APPEND_IN) */
return true;
}
diff --git a/core/module-text.c b/core/module-text.c
index 4ce1769..f09cb75 100644
--- a/core/module-text.c
+++ b/core/module-text.c
@@ -59,6 +59,30 @@ size_t pure_utf8_newlines(PureSpan span) {
return count;
}
+bool pure_parse_number(const unsigned char *text, size_t size, double *value) {
+ size_t i = 0;
+ if (i < size && text[i] == '-') ++i;
+ if (i == size || text[i] < '0' || text[i] > '9') return false;
+ if (text[i++] != '0') while (i < size && text[i] >= '0' && text[i] <= '9') ++i;
+ if (i < size && text[i] == '.') {
+ if (++i == size || text[i] < '0' || text[i] > '9') return false;
+ while (i < size && text[i] >= '0' && text[i] <= '9') ++i;
+ }
+ if (i < size && (text[i] == 'e' || text[i] == 'E')) {
+ if (++i < size && (text[i] == '+' || text[i] == '-')) ++i;
+ if (i == size || text[i] < '0' || text[i] > '9') return false;
+ while (i < size && text[i] >= '0' && text[i] <= '9') ++i;
+ }
+ if (i != size) return false;
+ char small[64], *digits = size < sizeof(small) ? small : malloc(size + 1); /* strtod rounds to nearest, ties to even */
+ if (!digits) return false;
+ memcpy(digits, text, size);
+ digits[size] = 0;
+ *value = strtod(digits, NULL);
+ if (digits != small) free(digits);
+ return isfinite(*value);
+}
+
size_t pure_format_number(double value, char out[32]) {
if (value == 0) { memcpy(out, "0", 2); return 1; }
if (value == floor(value) && fabs(value) < 1e15) {
diff --git a/core/module-text.h b/core/module-text.h
index 4843928..e718a40 100644
--- a/core/module-text.h
+++ b/core/module-text.h
@@ -24,6 +24,10 @@ size_t pure_utf8_newlines(PureSpan span);
* exponent. -0 prints as 0. Returns the length written (at most 32). */
size_t pure_format_number(double value, char out[32]);
+/* A number written as JSON writes one, -?(0|[1-9][0-9]*)(.[0-9]+)?([eE][+-]?[0-9]+)?, as the nearest double (ties to
+ * even): false where the text is not one or its value is not finite (027's NUM_PARSE). */
+bool pure_parse_number(const unsigned char *text, size_t size, double *value);
+
/* Positions in one version of a text draft (size is version.bytes). */
size_t pure_text_step(const PureDraft *, PureVersion, size_t position, double count);
size_t pure_text_line_step(const PureDraft *, PureVersion, size_t position, double count);
diff --git a/core/module.c b/core/module.c
index efa6547..db0703c 100644
--- a/core/module.c
+++ b/core/module.c
@@ -17,7 +17,7 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES =
* the named local holds, T_STATE for the type of the number, bool or i64
* state named. */
enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8, T_LOCAL = 9,
- T_BOOL = 10, T_STATE = 11, T_I64 = 12 };
+ T_BOOL = 10, T_STATE = 11, T_I64 = 12, T_LIST = 13 }; /* T_LIST: a list value, its owner the record of its items */
enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS, FROM_HELD };
/* Where an opcode may appear. */
/* IN_ARGUMENTS: fields read where nothing is done with them (a view's arguments, a message's fields in a fold). */
@@ -144,6 +144,11 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi
[PURE_OP_BYTE] = {1, {T_I64}, T_BYTES, READ, A_NONE, O_ANY},
[PURE_OP_BYTES_LOAD] = {0, {0}, T_BYTES, IN_LOCALS, A_HELD, O_ANY},
[PURE_OP_BYTES_STORE] = {1, {T_BYTES}, 0, IN_HANDLER | IN_COMPUTE, A_HELD, O_ANY},
+ [PURE_OP_LIST_VALUE] = {0, {0}, T_LIST, IN_HANDLER | IN_COMPUTE, STATE(LIST), O_ANY},
+ [PURE_OP_LIST_BIND] = {1, {T_LIST}, 0, IN_HANDLER | IN_COMPUTE, STATE(LIST), O_ANY},
+ /* APPEND_IN also takes as many values as the record of the list in the field has fields. */
+ [PURE_OP_APPEND_IN] = {1, {T_INDEX}, 0, IN_HANDLER | IN_COMPUTE, A_ITEM, O_LIST},
+ [PURE_OP_NUM_PARSE] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY},
};
#undef NUMS
#undef COMPARE
@@ -340,10 +345,44 @@ static bool state_of(const PureModule *m, uint32_t state, uint8_t type) {
static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t field) {
static const unsigned char types[] = {[PURE_RECORD_NUMBER] = T_NUM, [PURE_RECORD_TEXT] = T_TEXT,
- [PURE_RECORD_BOOL] = T_BOOL, [PURE_RECORD_I64] = T_I64};
+ [PURE_RECORD_BOOL] = T_BOOL, [PURE_RECORD_I64] = T_I64, [PURE_RECORD_LIST] = T_LIST};
return types[m->record_field_type[m->record[m->state_ref[list]].first + field]];
}
+/* The record of the lists a LIST field of a record holds (field: the module's numbering), or UINT32_MAX. */
+static uint32_t nested_record(const PureModule *m, uint32_t field) {
+ return m->record_field_type[field] == PURE_RECORD_LIST ? (uint32_t)m->record_field_default[field].i64 : UINT32_MAX;
+}
+
+/* An event's list, and one bound by LIST_BIND (a register: never changed by code). */
+static bool event_list_state(const PureModule *m, uint32_t list) {
+ return list < m->states && m->state_type[list] == PURE_STATE_LIST && m->state_flags[list] & PURE_LIST_ROWS >> 8;
+}
+static bool bound_list(const PureModule *m, uint32_t list) {
+ return event_list_state(m, list) && m->state_flags[list] & PURE_LIST_BOUND >> 8;
+}
+
+/* The stack type of a record field (the module's numbering). */
+static unsigned char record_type(const PureModule *m, uint32_t field) {
+ static const unsigned char types[] = {[PURE_RECORD_NUMBER] = T_NUM, [PURE_RECORD_TEXT] = T_TEXT,
+ [PURE_RECORD_BOOL] = T_BOOL, [PURE_RECORD_I64] = T_I64, [PURE_RECORD_LIST] = T_LIST};
+ return types[m->record_field_type[field]];
+}
+
+/* The record of the list value an instruction takes as its operand k (bottom 0). */
+static unsigned list_wanted(const PureModule *m, const PureOp *op, unsigned k) {
+ uint32_t arg = op->arg;
+ switch (op->op) {
+ case PURE_OP_LIST_BIND: return m->state_ref[arg];
+ case PURE_OP_LIST_APPEND: return nested_record(m, m->record[m->state_ref[arg]].first + k);
+ case PURE_OP_LIST_INSERT: return nested_record(m, m->record[m->state_ref[arg]].first + k - 1); /* the index first */
+ case PURE_OP_ITEM_SET: return nested_record(m, m->record[m->state_ref[list_of(arg)]].first + item_field(arg));
+ case PURE_OP_APPEND_IN:
+ return nested_record(m, m->record[nested_record(m, m->record[m->state_ref[list_of(arg)]].first + item_field(arg))].first + k - 1);
+ default: return UINT32_MAX;
+ }
+}
+
/* The stack type of a message field's value (a u32 is a number). */
static unsigned char message_type(uint8_t type) {
return type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : type == PURE_FIELD_BOOL ? T_BOOL :
@@ -517,6 +556,16 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
REFUSE("change of a keyed list's key");
if (op->op == PURE_OP_BYTES_OF && (m->record[m->state_ref[arg]].count != 1 || field_type(m, arg, 0) != T_I64))
REFUSE("bytes of a list not of one i64");
+ /* Lists in records (027's 2b): a register is read alone; values are of an event's lists; APPEND_IN appends to a
+ * list in a field of an event's list. */
+ if (((op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT || op->op == PURE_OP_LIST_REMOVE) && bound_list(m, arg)) ||
+ ((op->op == PURE_OP_ITEM_SET || op->op == PURE_OP_APPEND_IN) && bound_list(m, list_of(arg))))
+ 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");
/* 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) &&
@@ -575,6 +624,10 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
pops += record->count;
} else if (op->op == PURE_OP_ITEM_SET) {
need[pops++] = field_type(m, list_of(arg), item_field(arg));
+ } else if (op->op == PURE_OP_APPEND_IN) { /* an item of the list in the field */
+ const PureRecord *record = &m->record[nested_record(m, m->record[m->state_ref[list_of(arg)]].first + item_field(arg))];
+ for (unsigned f = 0; f < record->count; ++f) need[pops + f] = record_type(m, record->first + f);
+ pops += record->count;
} else if (need[0] == T_KEY) {
need[0] = field_type(m, arg, m->state_key[arg] - 1u);
} else if (need[0] == T_LOCAL) {
@@ -606,6 +659,8 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
height -= (int)pops;
for (unsigned k = 0; k < pops; ++k)
if (types[height + (int)k] != need[k]) REFUSE("operand type");
+ for (unsigned k = 0; k < pops; ++k) /* a list value goes where a list of its record goes */
+ if (need[k] == T_LIST && owners[height + (int)k] != list_wanted(m, op, k)) REFUSE("list of another record");
unsigned owner = of == O_ARG ? arg : of == O_REF ? m->state_ref[arg] : of == O_LIST ? list_of(arg) : 0;
if (kind == A_LOCAL && arg >> 8 & 0xFF) { /* an index local belongs to its list */
owner = arg >> 16;
@@ -634,6 +689,9 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
if (out) {
if (height == PURE_MODULE_STACK) REFUSE("stack overflow");
owners[height] = (unsigned char)(out != T_CURSOR && out != T_INDEX ? 0 : of == O_OPERAND ? owners[height] : owner);
+ if (out == T_LIST) /* the record of its items */
+ owners[height] = (unsigned char)(op->op == PURE_OP_LIST_VALUE ? m->state_ref[arg] :
+ nested_record(m, m->record[m->state_ref[list_of(arg)]].first + item_field(arg)));
types[height++] = out;
}
if (kind == A_TARGET) {
@@ -1116,11 +1174,11 @@ PURE_COLD static unsigned arity(uint8_t op) {
switch (op) {
case PURE_OP_CONST: case PURE_OP_LOAD: case PURE_OP_INDEX_LOAD: case PURE_OP_ROW: case PURE_OP_OUTER_ROW:
case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_FIELD: case PURE_OP_LOCAL_LOAD:
- case PURE_OP_BYTES_OF: case PURE_OP_BYTES_LOAD:
+ case PURE_OP_BYTES_OF: case PURE_OP_BYTES_LOAD: case PURE_OP_LIST_VALUE:
return 0;
case PURE_OP_ITEM_GET: case PURE_OP_LIST_FIND: case PURE_OP_LIST_HAS: case PURE_OP_NEG: case PURE_OP_NOT:
case PURE_OP_FLOOR: case PURE_OP_INDEX_NUM: case PURE_OP_I64_NEG: case PURE_OP_I64_FROM: case PURE_OP_NUM_FROM:
- case PURE_OP_INDEX_AT: case PURE_OP_BYTE:
+ case PURE_OP_INDEX_AT: case PURE_OP_BYTE: case PURE_OP_NUM_PARSE:
return 1;
case PURE_OP_CLAMP: case PURE_OP_BYTES_SLICE: return 3;
default: return 2;
@@ -1132,7 +1190,7 @@ PURE_COLD static int list_read(const PureOp *op) {
switch (op->op) {
case PURE_OP_ITEM_GET: return (int)list_of(op->arg);
case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_LIST_FIND:
- case PURE_OP_LIST_HAS: case PURE_OP_ROW: case PURE_OP_BYTES_OF: case PURE_OP_INDEX_AT:
+ case PURE_OP_LIST_HAS: case PURE_OP_ROW: case PURE_OP_BYTES_OF: case PURE_OP_INDEX_AT: case PURE_OP_LIST_VALUE:
return (int)op->arg;
default: return -1;
}
@@ -1331,8 +1389,10 @@ PURE_COLD static bool bounds_fixed(const PureModule *m, const PureRange *range,
states |= (uint64_t)1 << (op->arg & 63);
break;
case PURE_OP_LOCAL_STORE: case PURE_OP_LOOP: locals |= (uint64_t)1 << (op->arg & 63); break;
- case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: lists |= (uint64_t)1 << (op->arg & 63); break;
- case PURE_OP_ITEM_SET: lists |= (uint64_t)1 << (list_of(op->arg) & 63); break;
+ case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: case PURE_OP_LIST_BIND:
+ lists |= (uint64_t)1 << (op->arg & 63);
+ break;
+ case PURE_OP_ITEM_SET: case PURE_OP_APPEND_IN: lists |= (uint64_t)1 << (list_of(op->arg) & 63); break;
default: break;
}
}
@@ -1504,7 +1564,9 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
uint32_t flags = value ? 0 : read32(at + 12);
bool cursor = out->state_type[i] == PURE_STATE_CURSOR, list = out->state_type[i] == PURE_STATE_LIST;
bool rows = list && flags & PURE_LIST_ROWS; /* an event's list, keyed or not */
- uint32_t key = list ? flags & ~(uint32_t)PURE_LIST_ROWS : 0;
+ bool bound = list && flags & PURE_LIST_BOUND; /* a register: an event's list, not keyed */
+ uint32_t key = list ? flags & ~(uint32_t)(PURE_LIST_ROWS | PURE_LIST_BOUND) : 0;
+ if (bound && (!rows || key)) REFUSE("state type", i);
if (rows && type & PURE_STATE_TRANSIENT) REFUSE("state type", i); /* an event's own, saved or not */
/* A list of a message's rows is named by the field's path (input.field), no other state. */
if (!name_at(text, strings, read32(at), rows ? NAME_PATH : NAME_IDENTIFIER, out->state_names[i]) ||
@@ -1520,7 +1582,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
if ((flags && !(cursor && flags == PURE_CURSOR_STAYS) && !(list && key <= PURE_RECORD_FIELDS)) ||
read32(at + 8) > UINT16_MAX) REFUSE("state reference", i);
out->state_ref[i] = (uint16_t)read32(at + 8);
- out->state_flags[i] = cursor ? (uint8_t)flags : rows ? PURE_LIST_ROWS >> 8 : 0;
+ out->state_flags[i] = cursor ? (uint8_t)flags : (uint8_t)((rows ? PURE_LIST_ROWS >> 8 : 0) | (bound ? PURE_LIST_BOUND >> 8 : 0));
out->state_key[i] = (uint8_t)key;
}
}
@@ -1618,18 +1680,21 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
const unsigned char *entry = fields_at + (size_t)f * RECORD_FIELD_BYTES;
uint32_t type = read32(entry + 4); /* PureRecordType */
out->record_field_type[f] = (uint8_t)type;
- if (!name_at(text, strings, read32(entry), NAME_IDENTIFIER, out->record_field_names[f]) || type > PURE_RECORD_I64)
+ if (!name_at(text, strings, read32(entry), NAME_PATH, out->record_field_names[f]) || type > PURE_RECORD_LIST)
REFUSE("record field", f);
for (uint32_t k = record->first; k < f; ++k)
if (!strcmp(out->record_field_names[k], out->record_field_names[f])) REFUSE("duplicate record field", f);
/* A default: a text's constant, an i64's 8 bytes, a number (a bool's +0 or 1). */
if (type == PURE_RECORD_TEXT) out->record_field_default[f].num = read32(entry + 8);
- else if (type == PURE_RECORD_I64) out->record_field_default[f].i64 = (int64_t)(read32(entry + 8) | (uint64_t)read32(entry + 12) << 32);
+ else if (type == PURE_RECORD_I64 || type == PURE_RECORD_LIST) /* a LIST's: the record of its items */
+ out->record_field_default[f].i64 = (int64_t)(read32(entry + 8) | (uint64_t)read32(entry + 12) << 32);
else out->record_field_default[f].num = read_double(entry + 8);
double given = out->record_field_default[f].num;
if (type == PURE_RECORD_TEXT ? read32(entry + 12) || read32(entry + 8) >= out->constants
+ : type == PURE_RECORD_LIST ? (uint64_t)out->record_field_default[f].i64 >= out->records
: type != PURE_RECORD_I64 && (!isfinite(given) || (type == PURE_RECORD_BOOL && !canonical_bool(given))))
REFUSE("record field default", f);
+ if (type == PURE_RECORD_LIST) out->record_nested[i] = true;
}
next_record_field += record->count;
}
@@ -1644,6 +1709,19 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_BOOL))) ||
(out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST)))
REFUSE("state reference", i);
+ /* A list in a record is an event's alone (027's stage 2b): no stored, transient or message's list holds one, and no
+ * 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);
+ }
+ for (uint32_t f = 0; f < out->fields; ++f)
+ if (out->field[f].type == PURE_FIELD_ROWS &&
+ (out->record_nested[out->state_ref[out->field[f].rows]] || out->state_flags[out->field[f].rows] & PURE_LIST_BOUND >> 8))
+ REFUSE("rows field", f);
/* A stored position into what is not stored would come back meaning nothing. */
for (uint32_t i = 0; i < out->states; ++i)
if ((out->state_type[i] == PURE_STATE_CURSOR || out->state_type[i] == PURE_STATE_INDEX) &&
@@ -2171,6 +2249,53 @@ static int hold(PureApp *app, PureArena *arena, unsigned slot, PureBytes v, bool
return 0;
}
+/* ---- 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);
+
+/* 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) {
+ const PureRecord *r = &m->record[record];
+ for (unsigned f = 0; f < r->count; ++f)
+ if (m->record_field_type[r->first + f] == PURE_RECORD_LIST && cells[f].i64) ++app->nested.shared[cells[f].i64 - 1];
+}
+
+/* A new list of record's items among the event's (empty, or a copy of from's items): its number, or 0 out of memory. */
+static uint64_t nest(PureApp *app, uint32_t record, const PureList *from) {
+ const PureModule *m = app->module;
+ if (app->nested.count == app->nested.capacity) {
+ size_t capacity = app->nested.capacity ? app->nested.capacity * 2 : 16;
+ PureList **lists = realloc(app->nested.list, capacity * sizeof(*lists));
+ if (lists) app->nested.list = lists;
+ uint32_t *shared = lists ? realloc(app->nested.shared, capacity * sizeof(*shared)) : NULL;
+ if (!shared) return 0;
+ app->nested.shared = shared;
+ app->nested.capacity = capacity;
+ }
+ PureList *list = new_list(m, record);
+ 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) {
+ PureCell cells[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < r->count; ++f) cells[f] = pure_list_get(from, at, f);
+ if (pure_list_insert(list, at, cells, NULL) != PURE_OK) { pure_list_free(list); return 0; }
+ if (m->record_nested[record]) share_cells(app, m, record, cells);
+ }
+ app->nested.list[app->nested.count] = list;
+ app->nested.shared[app->nested.count] = 0;
+ return ++app->nested.count;
+}
+
+/* The bound lists given back their own, and the event's lists in records let go (an event or a view begins). */
+static void unbind(PureApp *app) {
+ for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i)
+ if (app->unbound[i]) app->lists[i] = app->unbound[i], app->unbound[i] = NULL;
+}
+static void drop_nested(PureApp *app) {
+ for (size_t i = 0; i < app->nested.count; ++i) pure_list_free(app->nested.list[i]);
+ app->nested.count = 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) {
@@ -3365,6 +3490,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->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;
@@ -3401,11 +3527,18 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
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;
+ 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 */
status = op->plan & PLAN_ROW ? pure_list_set_row(app->lists[list], at, field, cell, undo_for(app, list))
: pure_list_set(app->lists[list], at, field, cell, undo_for(app, list));
arena->used = mark;
if (status != PURE_OK) REFUSED;
+ if (nested) { /* the list it holds now is shared once more, the one it held once less */
+ if (cell.i64) ++app->nested.shared[cell.i64 - 1];
+ if (was) --app->nested.shared[was - 1];
+ }
if (!grows(app, before, list_held(app, app->lists[list]))) { kind = PURE_FAULT_MEMORY; FAILED; }
NEXT;
}
@@ -3628,6 +3761,68 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
if (held) STOPPED(PURE_ALLOCATION);
NEXT;
}
+ case PURE_OP_NUM_PARSE: { /* a text written as a JSON number, its nearest number */
+ PureSpan span;
+ size_t mark = arena->used;
+ if ((status = contiguous(app, arena, top->bytes, &span)) != PURE_OK) STOPPED(status);
+ double number;
+ bool read = pure_parse_number(span.data, span.size, &number);
+ arena->used = mark;
+ if (!read) { kind = PURE_FAULT_LIMIT; FAILED; }
+ top->num = number;
+ NEXT;
+ }
+ case PURE_OP_LIST_VALUE: { /* a copy of an event's list's items (an empty one is 0, made of nothing) */
+ if (!pure_list_count(app->lists[arg])) { stack[sp++].i64 = 0; NEXT; }
+ uint64_t made = nest(app, m->state_ref[arg], app->lists[arg]);
+ if (!made) STOPPED(PURE_ALLOCATION);
+ if (!grows(app, (Held){0, 0}, (Held){0, pure_list_size(app->nested.list[made - 1])})) { kind = PURE_FAULT_MEMORY; FAILED; }
+ stack[sp++].i64 = (int64_t)made;
+ NEXT;
+ }
+ case PURE_OP_LIST_BIND: { /* the register reads the list (shared: no one changes it in place from now) */
+ uint64_t list = (uint64_t)stack[--sp].i64;
+ if (!app->unbound[arg]) app->unbound[arg] = app->lists[arg];
+ app->lists[arg] = list ? app->nested.list[list - 1] : app->unbound[arg];
+ if (list) ++app->nested.shared[list - 1];
+ NEXT;
+ }
+ case PURE_OP_APPEND_IN: { /* an item into the list in a field, made the field's own first when another shares it */
+ uint32_t list = list_of(arg), field = item_field(arg), inner = nested_record(m, m->record[m->state_ref[list]].first + field);
+ const PureRecord *record = &m->record[inner];
+ 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; }
+ 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);
+ if (!own) STOPPED(PURE_ALLOCATION);
+ if (into) --app->nested.shared[into - 1];
+ app->nested.shared[own - 1] = 1;
+ if ((status = pure_list_set(app->lists[list], at, field, (PureCell){.i64 = (int64_t)own}, NULL)) != PURE_OK) REFUSED;
+ into = own;
+ }
+ PureList *in = app->nested.list[into - 1];
+ 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)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) {
+ PureSpan span = {0};
+ if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT &&
+ (status = contiguous(app, arena, stack[base + f].bytes, &span)) != PURE_OK)
+ STOPPED(status);
+ cells[f] = (PureCell){.i64 = stack[base + f].i64, .text = (unsigned char *)span.data, .size = span.size};
+ }
+ Held before = {0, pure_list_size(in)};
+ status = pure_list_insert(in, pure_list_count(in), cells, NULL);
+ arena->used = mark;
+ if (status != PURE_OK) REFUSED;
+ if (m->record_nested[inner]) share_cells(app, m, inner, cells);
+ if (!grows(app, before, (Held){0, pure_list_size(in)})) { kind = PURE_FAULT_MEMORY; FAILED; }
+ NEXT;
+ }
default: break;
}
double b = stack[--sp].num, a = stack[sp - 1].num, value;
@@ -4031,6 +4226,7 @@ void pure_snapshot_release(PureSnapshot *snapshot) {
static void let_own_go(PureApp *app);
void pure_app_stop(PureApp *app) {
+ unbind(app); /* a register's own list goes with the lists, the bound one with the event's */
pure_app_finish(app);
close_burst(app, true); /* unsaved changes are dropped with the app */
let_own_go(app);
@@ -4050,6 +4246,9 @@ void pure_app_stop(PureApp *app) {
}
pure_retired_free(&app->retired);
for (unsigned i = 0; i < PURE_MODULE_HELD; ++i) free(app->held[i].data);
+ drop_nested(app);
+ free(app->nested.list);
+ free(app->nested.shared);
pure_draft_close(app->stored_values);
free(app->carried);
pure_draft_close(app->stored_links);
@@ -4147,6 +4346,7 @@ static PureResult keep_reply(PureApp *app);
/* Every list of a message's rows emptied: an event's rows are its own. */
static void clear_rows(PureApp *app) {
const PureModule *m = app->module;
+ unbind(app);
for (uint32_t i = 0; i < m->states; ++i)
if (m->state_type[i] == PURE_STATE_LIST && m->state_flags[i] & PURE_LIST_ROWS >> 8) pure_list_clear(app->lists[i]);
}
@@ -4186,6 +4386,7 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
app->grown = app->spent = 0;
app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT;
for (unsigned i = 0; m->held && i < PURE_MODULE_HELD; ++i) app->held[i].size = 0;
+ if (app->nested.count) unbind(app), drop_nested(app); /* a view's body bound some on the app itself */
Mark mark;
mark_event(app, &mark);
memcpy(pending, app->values, sizeof(app->values));
@@ -4534,6 +4735,8 @@ static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) {
app->grown = app->spent = 0;
app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT;
for (unsigned i = 0; m->held && i < PURE_MODULE_HELD; ++i) app->held[i].size = 0;
+ unbind(app);
+ if (app->nested.count) drop_nested(app);
}
PureFault fault = app->fault;
uint32_t fault_pc = app->fault_pc;
@@ -4810,7 +5013,7 @@ PureSnapshot *pure_save_snapshot(PureApp *app, uint32_t runtime) {
static void let_own_go(PureApp *app) {
for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
pure_list_free(app->own[i]);
- app->own[i] = NULL;
+ app->own[i] = app->unbound[i] = NULL;
}
}
@@ -4826,7 +5029,8 @@ 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_BOOL] = PURE_CELL_BOOL, [PURE_RECORD_I64] = PURE_CELL_INTEGER,
+ [PURE_RECORD_LIST] = PURE_CELL_INTEGER}; /* a list in a record: its number */
return kinds[m->record_field_type[field]];
}
@@ -5223,6 +5427,8 @@ PURE_COLD PureResult pure_app_start(PureApp *app, const PureModule *module, Pure
}
PURE_COLD PureResult pure_app_replace(PureApp *app, const PureModule *module) {
+ unbind(app); /* lists are laid out again: none bound, the event's in records let go */
+ drop_nested(app);
pure_app_finish(app);
if (app->open || app->pending_count) {
PureResult saved = pure_app_save(app); /* the burst belongs to the module that made it */
diff --git a/core/module.h b/core/module.h
index 01bbb53..52cd4d8 100644
--- a/core/module.h
+++ b/core/module.h
@@ -45,6 +45,7 @@ typedef enum {
PURE_OP_LOOP, PURE_OP_BREAK,
PURE_OP_I64_BITS, PURE_OP_BYTE_AT, PURE_OP_BYTES_OF, PURE_OP_INDEX_AT,
PURE_OP_BYTES_SLICE, PURE_OP_BYTE, PURE_OP_BYTES_LOAD, PURE_OP_BYTES_STORE,
+ PURE_OP_LIST_VALUE, PURE_OP_LIST_BIND, PURE_OP_APPEND_IN, PURE_OP_NUM_PARSE,
PURE_OP_COUNT
} PureOpcode;
/* A bool is its own type (docs/design/016-bool-and-i64.md), held as a
@@ -90,6 +91,22 @@ typedef enum { PURE_BITS_AND, PURE_BITS_OR, PURE_BITS_XOR, PURE_BITS_LEFT, PURE_
* after the point of FIXED. A form of an integer takes an i64 or a whole
* 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).
+ * 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
+ * code) the list on the stack. APPEND_IN appends an item (its fields on the
+ * stack after an index) to the list in field (bits 16-31) of the item the
+ * index names in list (bits 0-15), an event's list. NUM_PARSE reads a text
+ * written as a JSON number as the nearest number (failing as LIMIT on any
+ * other text, or a value past the finite ones). */
typedef enum { PURE_FORMAT_PLAIN, PURE_FORMAT_DECIMAL, PURE_FORMAT_HEX, PURE_FORMAT_HEX_UPPER, PURE_FORMAT_BINARY,
PURE_FORMAT_OCTAL, PURE_FORMAT_FIXED } PureFormat;
enum { PURE_FORMAT_WIDTH = 8, PURE_FORMAT_ZEROS = 1 << 16, PURE_FORMAT_FORM = 17, PURE_FORMAT_DIGITS = 20 };
@@ -187,7 +204,7 @@ static inline uint8_t pure_value_kind(uint8_t state_type) {
state_type == PURE_STATE_I64 ? PURE_CELL_INTEGER : 0xFF;
}
/* A record field: a number, a text (its default a constant), a bool or an i64. */
-typedef enum { PURE_RECORD_NUMBER = 0, PURE_RECORD_TEXT = 1, PURE_RECORD_BOOL = 2, PURE_RECORD_I64 = 3 } PureRecordType;
+typedef enum { PURE_RECORD_NUMBER = 0, PURE_RECORD_TEXT = 1, PURE_RECORD_BOOL = 2, PURE_RECORD_I64 = 3, PURE_RECORD_LIST = 4 } PureRecordType;
/* A value's 8 bytes: a number (a bool's 0 or 1, a cursor's or an index's
* position too), or an integer. What a value is its state's or field's type
* says, and the verifier keeps to it: moves copy the bytes, and only what
@@ -199,7 +216,7 @@ typedef union { double num; int64_t i64; } PureValue;
* let.name): it is emptied when every event ends, and it is never saved
* nor logged for undoing. A message's rows field fills the one its path
* names (unkeyed) as the message arrives; code fills a let's (docs/design/018). */
-enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256 };
+enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256, PURE_LIST_BOUND = 512 };
/* A transient state (bit 8 of its type word in a module, a bit of
* PureModule.transient): the app holds it while it runs, and undoes it with
* a failed event, but never saves it: each start begins it at its initial
@@ -327,7 +344,9 @@ typedef struct {
PureRecord record[PURE_MODULE_RECORDS];
char record_field_names[PURE_MODULE_RECORD_FIELDS][PURE_MODULE_NAME];
uint8_t record_field_type[PURE_MODULE_RECORD_FIELDS]; /* PureRecordType */
- PureValue record_field_default[PURE_MODULE_RECORD_FIELDS]; /* a number (a bool's 0 or 1), an i64, or a text constant's index */
+ PureValue record_field_default[PURE_MODULE_RECORD_FIELDS]; /* a number (a bool's 0 or 1), an i64, a text constant's index
+ * or a list's record (LIST, its i64) */
+ bool record_nested[PURE_MODULE_RECORDS]; /* a record with a LIST field (set at load) */
} PureModule;
/* Whether a state of m is transient (PURE_STATE_TRANSIENT). */
@@ -522,6 +541,10 @@ typedef struct {
bool reading; /* a reader: its states are a snapshot's */
/* The values an event or a view's body holds by name (BYTES_STORE): emptied as it begins. */
struct { unsigned char *data; size_t size, capacity; } held[PURE_MODULE_HELD];
+ /* The lists in records an event or a view's body made (LIST fields: a value is a number here, 1 on), how many
+ * values share each, and the bound lists' own (a register's list while another is bound to it). */
+ struct { PureList **list; uint32_t *shared; size_t count, capacity; } nested;
+ PureList *unbound[PURE_MODULE_STATES];
} PureApp;
PureResult pure_app_start(PureApp *, const PureModule *, PureData data);
diff --git a/core/test-module.c b/core/test-module.c
index 02d5015..f45cada 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -180,7 +180,7 @@ static size_t build(unsigned char *out, Spec spec) {
p = put32(p, offset[++n]);
p = put32(p, field->text);
if (field->text == 1) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); }
- else if (field->text == 3) {
+ else if (field->text == 3 || field->text == 4) { /* an i64's default, a list's record */
int64_t initial = (int64_t)field->initial;
p = put32(p, (uint32_t)(uint64_t)initial);
p = put32(p, (uint32_t)((uint64_t)initial >> 32));
@@ -345,7 +345,7 @@ static void rejections(void) {
static const O nan_const[] = {{C, 0, NAN}, {ST, 0, 0}};
static const O noncanon[] = {{C, 0, 1}, {NEG, 1, 0}, {ST, 0, 0}};
static const O unreachable[] = {{JMP, 2, 0}, {C, 0, 1}, {C, 0, 1}, {ST, 0, 0}};
- static const O bad_op[] = {{99, 0, 0}, {ST, 0, 0}};
+ static const O bad_op[] = {{PURE_OP_COUNT, 0, 0}, {ST, 0, 0}}; /* past the last opcode */
static const H h_all[] = {{0, 0, 1}};
static const H h_two[] = {{0, 0, 2}};
static const H h_five[] = {{0, 0, 5}};
@@ -6247,6 +6247,77 @@ static void formats_and_held(void) {
printf("{\"formats_and_held\":{\"formats\":%zu,\"slices\":%zu,\"refused\":%zu}}\n", LEN(shown), LEN(slices), LEN(refused));
}
+/* ---- Lists in records (docs/design/027, stage 2b) ------------------------
+ * A Node's kids are a list of Leaf. A list made a value (LIST_VALUE) is a
+ * copy; a value stored in a second cell shares the list, and an append into
+ * either (APPEND_IN) makes that cell's own first; a register (LIST_BIND)
+ * reads the list as it was bound, appends after it go to a copy. */
+enum { LV = PURE_OP_LIST_VALUE, BIND = PURE_OP_LIST_BIND, AIN = PURE_OP_APPEND_IN };
+#define NEXT_DIGIT(list) {L, 0, 0}, {C, 0, 10}, {MUL, 0, 0}, {LC, list, 0}, {ADD, 0, 0}, {ST, 0, 0}
+
+static void lists_in_records(void) {
+ static const S nums[] = {{"n", 0}};
+ static const RF leaf_fields[] = {{"v", 3, 0}};
+ static const RF node_fields[] = {{"name", 3, 0}, {"kids", 4, 0}}; /* kids: a list of record 0, Leaf */
+ static const R records[] = {{"Leaf", leaf_fields, 1}, {"Node", node_fields, 2}};
+ static const T others[] = {{"let.nodes", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, {"let.leaves", 3 | (uint32_t)PURE_LIST_ROWS << 8, 0},
+ {"let.view", 3 | (uint32_t)(PURE_LIST_ROWS | PURE_LIST_BOUND) << 8, 0}, {"i", PURE_STATE_I64, 0}};
+ static const I inputs[] = {KEY("go", "KeyG")};
+ const O code[] = {
+ {C, 0, 0}, {ST, 0, 0}, /* 0 n = 0 */
+ IC(7), {LA, 2, 0}, IC(9), {LA, 2, 0}, /* 2 leaves = [7, 9] */
+ IC(1), {LV, 2, 0}, {LA, 1, 0}, /* 6 nodes.append(Node { 1, leaves }) */
+ IC(11), {LA, 2, 0}, /* 9 leaves.append(11): kids keep 2 */
+ {XF, 1, 0}, IC(5), {AIN, ITEM(1, 1), 0}, /* 11 nodes[0].kids += 5 */
+ IC(2), {XF, 1, 0}, {IG, ITEM(1, 1), 0}, {LA, 1, 0}, /* 14 nodes.append(Node { 2, nodes[0].kids }) */
+ {C, 0, 1}, {XAT, 1, 0}, IC(6), {AIN, ITEM(1, 1), 0}, /* 18 nodes[1].kids += 6: its own first */
+ {XF, 1, 0}, {IG, ITEM(1, 1), 0}, {BIND, 3, 0}, /* 22 view = nodes[0].kids (3) */
+ NEXT_DIGIT(3), /* 25 */
+ {XF, 1, 0}, IC(8), {AIN, ITEM(1, 1), 0}, /* 31 nodes[0].kids += 8: bound, so a copy */
+ NEXT_DIGIT(3), /* 34 the view still 3 */
+ {XF, 1, 0}, {IG, ITEM(1, 1), 0}, {BIND, 3, 0}, NEXT_DIGIT(3), /* 40 nodes[0].kids now 4 */
+ {C, 0, 1}, {XAT, 1, 0}, {IG, ITEM(1, 1), 0}, {BIND, 3, 0}, NEXT_DIGIT(3), /* 49 nodes[1].kids: 7 9 5 6 */
+ {FOLD, FOLDING(PURE_FOLD_SUM | PURE_FOLD_INTEGER, 3, 3), 0}, {ROW, 3, 0}, {IG, ITEM(3, 0), 0}, {C, 1, 1}, {ST, 4, 0},
+ NEXT_DIGIT(2), /* 64 leaves: 3 */
+ };
+ H handler = {1, 0, LEN(code)};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, code, LEN(code), NULL, 0, others, LEN(others), NULL, 0, NULL, 0,
+ records, 2, NULL, 0};
+ if (load(spec, &module_b) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_b, (PureData){0}) == PURE_OK);
+ for (int round = 0; round < 3; ++round) { /* each event begins with its lists and theirs empty */
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ if (app.values[0].num != 33443 || app.values[4].i64 != 27) {
+ fprintf(stderr, "lists in records: n %g, i %" PRId64 "\n", app.values[0].num, app.values[4].i64);
+ abort();
+ }
+ CHECK(pure_list_count(app.lists[1]) == 0 && app.lists[3] && pure_list_count(app.lists[3]) == 0);
+ }
+ pure_app_stop(&app);
+ /* Refused: a change of a register, a bind to a list not bound, an append into a field not a list, a list of another
+ * record; a stored list of records holding lists. */
+ const O refused[][4] = {
+ {IC(1), {LA, 3, 0}, {C, 0, 0}, {ST, 0, 0}},
+ {{XF, 1, 0}, {IG, ITEM(1, 1), 0}, {BIND, 2, 0}, {C, 0, 0}},
+ {{XF, 1, 0}, IC(1), {AIN, ITEM(1, 0), 0}, {C, 0, 0}},
+ {IC(1), {LV, 1, 0}, {LA, 1, 0}, {C, 0, 0}},
+ {{LV, 4, 0}, {C, 0, 0}, {C, 0, 0}, {C, 0, 0}},
+ };
+ for (size_t k = 0; k < LEN(refused); ++k) {
+ H one = {1, 0, 3};
+ Spec bad = {nums, 1, inputs, 1, NULL, 0, &one, 1, refused[k], 3, NULL, 0, others, LEN(others), NULL, 0, NULL, 0,
+ records, 2, NULL, 0};
+ if (load(bad, &module_b) == PURE_OK) { fprintf(stderr, "lists in records: refused %zu loaded\n", k); abort(); }
+ }
+ static const T stored[] = {{"nodes", 3, 1}};
+ 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);
+}
+
/* 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 };
@@ -6622,6 +6693,7 @@ int main(void) {
integers();
bits_and_bytes();
formats_and_held();
+ lists_in_records();
memory_budget();
work_budget();
fold_mutations();