aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-05 06:54:21 +0100
committereunhhu <[email protected]>2026-10-05 06:54:21 +0100
commit07791a8b9f9c25e83ef849e1cea431ed25be7553 (patch)
treef6bd1fa52ce5f3dec163a8df5ff43d62372480a0
parentce4be172725d0aca0aa55409d4077bf788374dde (diff)
downloadpure-07791a8b9f9c25e83ef849e1cea431ed25be7553.tar.gz
pure-07791a8b9f9c25e83ef849e1cea431ed25be7553.zip
Keep an event's lists in arrays, flat or at their keys, and read a field by key as one (021)
- pure_list_arrays: an event's list (never shared, journaled or undone) keeps its items in arrays: FLAT in order, or DENSE at their keys when keyed by small whole numbers. A position asked of a DENSE list makes it FLAT; moves past a few times its items, arrays past 64 MiB, or a third change of form make it a tree. Answers are the tree's; the stored bytes stand for its items in the memory budget whatever it keeps them in. - pure_list_add need not say where an item went: a list no index state refers to is not asked. - A LIST_FIND whose index the next ITEM_GET of its list takes at once is joined to it at load (PLAN_JOINED): pure_list_lookup reads the field by key, at the key in a DENSE list. - test-module arrays_equivalence: beside a tree, every step of a random run (adds, inserts, removes, changes, finds, reads, runs, clears) gives the same answers through every form and change of form. - A search (100,000 nodes, 1,000,000 edges, core only): 187 to 126 ms.
-rw-r--r--core/module-list.c452
-rw-r--r--core/module-list.h21
-rw-r--r--core/module.c48
-rw-r--r--core/module.h1
-rw-r--r--core/test-module.c116
5 files changed, 620 insertions, 18 deletions
diff --git a/core/module-list.c b/core/module-list.c
index ea15fb1..0d53c60 100644
--- a/core/module-list.c
+++ b/core/module-list.c
@@ -81,6 +81,18 @@ struct PureList {
uint64_t *members;
size_t member_capacity, member_count;
bool bitmap;
+ /* An event's list may keep its items in arrays rather than a tree
+ * (pure_list_arrays, docs/design/021): FLAT, each field's cells side by
+ * side in the list's order; DENSE (keyed by small whole numbers, its keys
+ * as bits), each field's cells at its key. What the arrays cannot answer
+ * at once (a position in a DENSE list; a change logged to be undone, a
+ * record, a share) makes them FLAT, or a tree, first. */
+ uint8_t form;
+ bool arrays; /* it may be FLAT or DENSE: an event's list */
+ uint8_t flattened; /* DENSE made FLAT since it was cleared: twice, and it stays FLAT or a tree */
+ void *columns[16]; /* each field's cells */
+ size_t room; /* the rows the columns hold: FLAT items, DENSE keys */
+ size_t moved; /* FLAT: items moved by changes inside it since it was cleared */
};
/* The log of an event's changes: a removed item's cells wait in the log's
@@ -738,6 +750,249 @@ static void take_out(PureList *list, Step *path, unsigned depth, unsigned within
++list->version;
}
+/* ---- An event's list in arrays (docs/design/021) */
+
+enum { TREE, FLAT, DENSE };
+/* The arrays a clear keeps for the next event; the most they grow to (a
+ * larger list is a tree: doubling arrays would hold twice its items); a
+ * DENSE list's slack beyond four times its items (so it never holds far more
+ * than a tree would); the items a FLAT list moves freely. */
+enum { ARRAYS_KEPT = 1 << 22, ARRAYS_LARGE = 1 << 26, DENSE_SLACK = 1 << 21, FLAT_SMALL = 64 };
+
+static size_t width(const Layout *l, unsigned f) { return pure_cell_text(l->kinds[f]) ? sizeof(PureSpan) : 8; }
+
+static size_t row_width(const Layout *l) {
+ size_t row = 0;
+ for (unsigned f = 0; f < l->fields; ++f) row += width(l, f);
+ return row ? row : 1;
+}
+
+static PureCell array_cell(const PureList *list, size_t i, unsigned f) {
+ PureCell cell = {0};
+ if (!pure_cell_text(list->layout.kinds[f])) memcpy(&cell.i64, (const char *)list->columns[f] + 8 * i, 8);
+ else {
+ PureSpan text = ((const PureSpan *)list->columns[f])[i];
+ cell.text = (unsigned char *)text.data, cell.size = text.size;
+ }
+ return cell;
+}
+
+static void array_put(PureList *list, size_t i, unsigned f, PureCell cell) {
+ if (pure_cell_text(list->layout.kinds[f])) ((PureSpan *)list->columns[f])[i] = (PureSpan){cell.text, cell.size};
+ else memcpy((char *)list->columns[f] + 8 * i, &cell.i64, 8);
+}
+
+/* Row from's cells to row to, of the same columns or of others. */
+static void array_copy(const Layout *l, void *const *to_columns, size_t to, void *const *from_columns, size_t from) {
+ for (unsigned f = 0; f < l->fields; ++f)
+ memcpy((char *)to_columns[f] + to * width(l, f), (const char *)from_columns[f] + from * width(l, f), width(l, f));
+}
+
+static void array_move(PureList *list, size_t to, size_t from, size_t count) {
+ const Layout *l = &list->layout;
+ for (unsigned f = 0; f < l->fields; ++f)
+ memmove((char *)list->columns[f] + to * width(l, f), (const char *)list->columns[f] + from * width(l, f),
+ count * width(l, f));
+}
+
+/* Columns of room rows each; false without memory (each as it was or grown). */
+static bool arrays_room(PureList *list, size_t room) {
+ const Layout *l = &list->layout;
+ for (unsigned f = 0; f < l->fields; ++f) {
+ void *grown = realloc(list->columns[f], room * width(l, f));
+ if (!grown) return false;
+ list->columns[f] = grown;
+ }
+ list->room = room;
+ return true;
+}
+
+static void arrays_free(PureList *list) {
+ for (unsigned f = 0; f < 16; ++f) free(list->columns[f]), list->columns[f] = NULL;
+ list->room = 0;
+}
+
+/* A DENSE list's i-th key's slot, counted along its bits (no memory needed). */
+static size_t dense_slot(const PureList *list, size_t i) {
+ for (size_t w = 0; w < list->member_capacity; ++w) {
+ size_t bits = (size_t)__builtin_popcountll(list->members[w]);
+ if (i < bits) {
+ uint64_t word = list->members[w];
+ for (; i; --i) word &= word - 1;
+ return w * 64 + (size_t)__builtin_ctzll(word);
+ }
+ i -= bits;
+ }
+ return SIZE_MAX;
+}
+
+/* The texts of every item of a FLAT or DENSE list let go. */
+static void let_go_rows(PureList *list) {
+ const Layout *l = &list->layout;
+ if (!l->texts) return;
+ for (size_t i = 0; list->form == FLAT && i < list->count; ++i)
+ for (unsigned t = 0; t < l->texts; ++t) free_text(array_cell(list, i, l->text_field[t]).text);
+ for (size_t w = 0; list->form == DENSE && w < list->member_capacity; ++w)
+ for (uint64_t word = list->members[w]; word; word &= word - 1)
+ for (unsigned t = 0; t < l->texts; ++t)
+ free_text(array_cell(list, w * 64 + (size_t)__builtin_ctzll(word), l->text_field[t]).text);
+}
+
+/* The keys a DENSE list may hold: below four times its items and a slack. */
+static size_t dense_limit(const PureList *list) {
+ size_t row = row_width(&list->layout), limit = 4 * (list->count + 1) + DENSE_SLACK / row;
+ return limit < ARRAYS_LARGE / row ? limit : ARRAYS_LARGE / row;
+}
+
+/* Whether a full FLAT list would grow its arrays past ARRAYS_LARGE. */
+static bool flat_large(const PureList *list) {
+ return list->count == list->room && 2 * list->room * row_width(&list->layout) > ARRAYS_LARGE;
+}
+
+static bool dense_holds(const PureList *list, double key) { return small_key(key) && (size_t)key < dense_limit(list); }
+
+/* A FLAT list keyed by small whole numbers (its keys as bits) as DENSE,
+ * room for key too; false when it cannot be (it stays FLAT). */
+static bool make_dense(PureList *list, double key) {
+ const Layout *l = &list->layout;
+ if (l->key < 0 || !list->members || !list->bitmap || list->flattened >= 2 || !dense_holds(list, key)) return false;
+ const double *keys = list->columns[l->key];
+ size_t top = (size_t)key;
+ if (list->count) {
+ double last = keys[list->count - 1]; /* the greatest: FLAT keeps key order */
+ if (!dense_holds(list, last)) return false;
+ if ((size_t)last > top) top = (size_t)last;
+ }
+ size_t room = top + 1 + (top + 1) / 2, limit = dense_limit(list);
+ if (room > limit) room = limit;
+ if (room <= top) return false;
+ void *columns[16] = {0};
+ for (unsigned f = 0; f < l->fields; ++f)
+ if (!(columns[f] = malloc(room * width(l, f)))) {
+ for (unsigned g = 0; g < f; ++g) free(columns[g]);
+ return false;
+ }
+ for (size_t i = 0; i < list->count; ++i) array_copy(l, columns, (size_t)keys[i], list->columns, i);
+ arrays_free(list);
+ memcpy(list->columns, columns, sizeof(columns));
+ list->room = room;
+ list->form = DENSE;
+ return true;
+}
+
+/* A DENSE list as FLAT, its items in key order; false without memory. */
+static bool make_flat(PureList *list) {
+ const Layout *l = &list->layout;
+ size_t room = list->count > 16 ? list->count : 16, row = 0;
+ void *columns[16] = {0};
+ for (unsigned f = 0; f < l->fields; ++f)
+ if (!(columns[f] = malloc(room * width(l, f)))) {
+ for (unsigned g = 0; g < f; ++g) free(columns[g]);
+ return false;
+ }
+ for (size_t w = 0; w < list->member_capacity; ++w)
+ for (uint64_t word = list->members[w]; word; word &= word - 1)
+ array_copy(l, columns, row++, list->columns, w * 64 + (size_t)__builtin_ctzll(word));
+ arrays_free(list);
+ memcpy(list->columns, columns, sizeof(columns));
+ list->room = room;
+ list->form = FLAT;
+ list->moved = 0;
+ ++list->flattened;
+ return true;
+}
+
+/* Every node below node freed, its texts not (they are a FLAT list's still). */
+static void free_nodes(Node *node) {
+ if (!node) return;
+ for (unsigned i = 0; !node->leaf && i < node->count; ++i) free_nodes(child(node, i));
+ free(node);
+}
+
+/* A FLAT or DENSE list as a tree, its items placed in order (a tree that
+ * grows at its end fills its leaves); false without memory (it is as it was). */
+static bool make_tree(PureList *list) {
+ if (list->form == TREE) return true;
+ if (list->form == DENSE && !make_flat(list)) return false;
+ size_t count = list->count;
+ list->form = TREE;
+ list->count = 0;
+ for (size_t i = 0; i < count; ++i) {
+ PureCell cells[16];
+ for (unsigned f = 0; f < list->layout.fields; ++f) cells[f] = array_cell(list, i, f);
+ Place p;
+ if (!prepare(list, list->count, &p)) {
+ free_nodes(list->root);
+ list->root = NULL;
+ list->count = count;
+ list->form = FLAT;
+ return false;
+ }
+ place(list, &p, cells);
+ }
+ arrays_free(list);
+ return true;
+}
+
+/* An item (its cells the list's own) into a FLAT list before at. */
+static bool flat_insert(PureList *list, size_t at, const PureCell *item) {
+ if (list->count == list->room && !arrays_room(list, list->room ? 2 * list->room : 16)) return false;
+ if (at < list->count) {
+ array_move(list, at + 1, at, list->count - at);
+ list->moved += list->count - at;
+ }
+ for (unsigned f = 0; f < list->layout.fields; ++f) array_put(list, at, f, item[f]);
+ if (list->members) member_add(list, item[list->layout.key].num);
+ ++list->count;
+ ++list->version;
+ return true;
+}
+
+/* Whether a FLAT list may move the items after at (else it becomes a tree):
+ * a few always, more while the moves stay within a few times its items. */
+static bool flat_moves(const PureList *list, size_t at) {
+ size_t after = list->count - at;
+ return list->count < FLAT_SMALL || list->moved + after <= 8 * list->count + 4096;
+}
+
+/* An item (its cells the list's own) into a DENSE list at its key (one it
+ * may hold); false without memory (the list as it was). */
+static bool dense_add(PureList *list, const PureCell *item, double number) {
+ size_t key = (size_t)number;
+ if (key >= list->room) {
+ size_t room = list->room + list->room / 2, limit = dense_limit(list);
+ if (room <= key) room = key + 1 + (key + 1) / 2;
+ if (room > limit) room = limit;
+ if (!arrays_room(list, room)) return false;
+ }
+ if (key / 64 >= list->member_capacity) { /* the bits grown first: adding a key must not give them up */
+ size_t capacity = list->member_capacity ? list->member_capacity : 1;
+ while (capacity <= key / 64) capacity *= 2;
+ uint64_t *grown = realloc(list->members, capacity * sizeof(*grown));
+ if (!grown) return false;
+ memset(grown + list->member_capacity, 0, (capacity - list->member_capacity) * sizeof(*grown));
+ list->members = grown, list->member_capacity = capacity;
+ }
+ for (unsigned f = 0; f < list->layout.fields; ++f) array_put(list, key, f, item[f]);
+ member_add(list, number);
+ ++list->count;
+ ++list->version;
+ return true;
+}
+
+/* The list as a form that answers by position (FLAT or a tree): a DENSE
+ * one made FLAT, a tree when it was made FLAT too often; false without memory. */
+static bool positioned(PureList *list) {
+ if (list->form != DENSE) return true;
+ if (list->flattened >= 2) return make_tree(list);
+ return make_flat(list);
+}
+
+/* The list as a tree, for a change logged to be undone or recorded. */
+static bool logged(PureList *list, const PureUndo *undo) {
+ return list->form == TREE || (!undo && !list->journaled) || make_tree(list);
+}
+
/* ---- The list */
PureList *pure_list_new(unsigned fields, const uint8_t *kinds) {
@@ -774,6 +1029,7 @@ PureResult pure_list_keyed(PureList *list, int field) {
Layout *l = &list->layout;
int key = field >= 0 && (unsigned)field < l->fields && l->kinds[field] == PURE_CELL_NUMBER ? field : -1;
if (key == l->key) return PURE_OK;
+ if (list->count && !make_tree(list)) return PURE_ALLOCATION;
l->key = key;
if (list->root && !rekey(list, &list->root)) {
l->key = -1; /* searched without leads, which some nodes lack */
@@ -799,6 +1055,8 @@ void pure_list_free(PureList *list) {
if (list->copy) {
if (__atomic_sub_fetch(&list->refs, 1, __ATOMIC_ACQ_REL)) return;
} else {
+ if (list->form != TREE) let_go_rows(list);
+ arrays_free(list);
free(list->journal);
free(list->members);
let_go_tree(list, list->root);
@@ -811,11 +1069,21 @@ void pure_list_free(PureList *list) {
}
void pure_list_clear(PureList *list) {
- if (!list->count) return;
- let_go_tree(list, list->root);
- list->root = NULL;
- list->count = list->bytes = 0;
- ++list->version;
+ bool had = list->count != 0;
+ if (had) {
+ if (list->form != TREE) let_go_rows(list);
+ let_go_tree(list, list->root);
+ list->root = NULL;
+ list->count = list->bytes = 0;
+ ++list->version;
+ }
+ if (list->arrays) { /* FLAT again, its arrays kept for the next event unless large */
+ list->form = FLAT;
+ list->flattened = 0;
+ list->moved = 0;
+ if (list->room * row_width(&list->layout) > ARRAYS_KEPT) arrays_free(list);
+ }
+ if (!had) return;
if (!list->members) return;
if (list->bitmap && list->member_capacity <= BITMAP_KEPT) { /* small bits are kept for the next event */
memset(list->members, 0, list->member_capacity * sizeof(*list->members));
@@ -849,11 +1117,37 @@ bool pure_list_contains(const PureList *list, double key, bool *found) {
return true;
}
+/* An event's arrays are its items, as a tree's nodes are: neither counts
+ * here (the stored bytes stand for both), so a list's room runs out at the
+ * same item whatever it keeps them in (021). */
size_t pure_list_extra(const PureList *list) { return list->member_capacity * sizeof(*list->members); }
+bool pure_list_arrays(PureList *list) {
+ if (list->form != TREE || list->arrays) return true;
+ if (list->copy || list->journaled || list->count) return false;
+ list->arrays = true;
+ list->form = FLAT;
+ return true;
+}
+
+unsigned pure_list_form(const PureList *list) { return list->form; }
+
+bool pure_list_lookup(const PureList *list, unsigned key_field, PureCell key, unsigned field, PureCell *cell) {
+ bool found;
+ if (list->form == DENSE && (int)key_field == list->layout.key) { /* at its key, by its bits */
+ if (!pure_list_contains(list, key.num, &found) || !found) return false;
+ *cell = array_cell(list, (size_t)key.num, field);
+ return true;
+ }
+ size_t at = pure_list_seek(list, key_field, key, &found);
+ if (found) *cell = pure_list_get(list, at, field);
+ return found;
+}
+
size_t pure_list_count(const PureList *list) { return list->count; }
PureList *pure_list_share(PureList *list, PureRetired *retired, uint64_t epoch) {
+ if (!make_tree(list)) return NULL; /* a copy reads a tree */
PureList *copy = malloc(sizeof(*copy));
if (!copy) return NULL;
*copy = (PureList){.layout = list->layout, .root = list->root, .count = list->count, .bytes = list->bytes,
@@ -1063,6 +1357,8 @@ static unsigned char *record_head(unsigned char *out, unsigned kind, size_t at)
}
void pure_list_journal(PureList *list, bool journaled) {
+ if (journaled && list->form != TREE && !make_tree(list)) abort(); /* an event's list is never journaled */
+ if (journaled) list->arrays = false;
list->journaled = journaled;
if (!journaled) pure_list_forget(list, 0);
}
@@ -1099,12 +1395,20 @@ static bool encode_leaf(void *context, const Node *leaf) {
size_t pure_list_size(const PureList *list) { return list->bytes; }
+/* An event's list is never stored, shared or sorted: one that was is made a
+ * tree first (memory for it or none: the process stops rather than lose items). */
+static void tree_now(const PureList *list) {
+ if (list->form != TREE && !make_tree((PureList *)list)) abort();
+}
+
void pure_list_encode(const PureList *list, unsigned char *out) {
+ tree_now(list);
Encoding encoding = {&list->layout, out};
visit_leaves(list->root, encode_leaf, &encoding);
}
size_t pure_list_encode_part(const PureList *list, size_t *at, unsigned char *out, size_t capacity, size_t *needed) {
+ tree_now(list);
const Layout *l = &list->layout;
PureRun run;
size_t used = 0;
@@ -1135,6 +1439,7 @@ static bool decode_item(const Layout *l, const unsigned char *bytes, size_t size
}
PURE_COLD PureResult pure_list_read(PureList *list, const unsigned char *bytes, size_t size) {
+ if (!make_tree(list)) return PURE_ALLOCATION;
PureCell cells[16];
for (size_t at = 0; at < size;) {
if (!decode_item(&list->layout, bytes, size, &at, cells)) return PURE_STORE_ERROR;
@@ -1151,6 +1456,7 @@ static bool record_cell(const Layout *l, bool compact, unsigned field, const uns
}
PURE_COLD PureResult pure_list_replay(PureList *list, const unsigned char *bytes, size_t size) {
+ if (!make_tree(list)) return PURE_ALLOCATION;
const Layout *l = &list->layout;
bool journaled = list->journaled;
PureResult result = PURE_OK;
@@ -1188,10 +1494,21 @@ PURE_COLD PureResult pure_list_replay(PureList *list, const unsigned char *bytes
PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, PureUndo *undo) {
const Layout *l = &list->layout;
if (at > list->count || list->count == PURE_LIST_ITEMS) return PURE_LIMIT;
+ if (!logged(list, undo) || !positioned(list) ||
+ (list->form == FLAT && ((at < list->count && !flat_moves(list, at)) || flat_large(list)) && !make_tree(list)))
+ return PURE_ALLOCATION;
PureCell item[16];
unsigned owned = 0;
for (; owned < l->fields; ++owned)
if (!own_cell(list, owned, cells[owned], &item[owned])) break;
+ if (list->form == FLAT) {
+ if (owned < l->fields || !flat_insert(list, at, item)) {
+ for (unsigned f = 0; f < owned; ++f) free_text(item[f].text);
+ return PURE_ALLOCATION;
+ }
+ list->bytes += cells_bytes(l, item);
+ return PURE_OK;
+ }
size_t size = RECORD_HEAD;
for (unsigned f = 0; list->journaled && f < l->fields; ++f) size += compact_bound(l, f, cells[f]);
unsigned char *record = list->journaled ? journal_room(list, size) : NULL;
@@ -1219,16 +1536,54 @@ PureResult pure_list_add(PureList *list, const PureCell *cells, PureUndo *undo,
const Layout *l = &list->layout;
*exists = false;
if (l->key < 0) return PURE_INVALID; /* searched and inserted apart, then */
+ if (!logged(list, undo)) return PURE_ALLOCATION;
+ if (list->form != TREE) {
+ double key = cells[l->key].num;
+ bool found = false;
+ if (list->form == DENSE) { /* by its bits, at its key: no position, unless asked */
+ if (pure_list_contains(list, key, &found) && found && !at) return *exists = true, PURE_OK;
+ if (!at && !found && dense_holds(list, key)) {
+ PureCell item[16];
+ unsigned owned = 0;
+ for (; owned < l->fields; ++owned)
+ if (!own_cell(list, owned, cells[owned], &item[owned])) break;
+ if (owned < l->fields || !dense_add(list, item, key)) {
+ for (unsigned f = 0; f < owned; ++f) free_text(item[f].text);
+ return PURE_ALLOCATION;
+ }
+ list->bytes += cells_bytes(l, item);
+ return PURE_OK;
+ }
+ if (!positioned(list)) return PURE_ALLOCATION;
+ }
+ if (list->form == FLAT) {
+ const double *keys = list->columns[l->key];
+ size_t n = list->count, low = n ? lower(keys, (unsigned)n, key) : 0;
+ if (low < n && keys[low] == key) {
+ if (at) *at = low;
+ return *exists = true, PURE_OK;
+ }
+ if (low < n && !at && n >= FLAT_SMALL && make_dense(list, key)) return pure_list_add(list, cells, undo, at, exists);
+ if (low < n && !flat_moves(list, low) && !make_tree(list)) return PURE_ALLOCATION;
+ if (list->form == FLAT) {
+ if (at) *at = low;
+ return pure_list_insert(list, low, cells, undo);
+ }
+ }
+ }
if (list->members) { /* the set says at once when the key is there */
bool found;
if (pure_list_contains(list, cells[l->key].num, &found) && found) {
- *at = pure_list_seek(list, (unsigned)l->key, cells[l->key], exists);
+ size_t where = pure_list_seek(list, (unsigned)l->key, cells[l->key], exists);
+ if (at) *at = where;
return PURE_OK;
}
}
if (list->count == PURE_LIST_ITEMS) return PURE_LIMIT;
if (!list->root && !(list->root = new_node(list, true))) return PURE_ALLOCATION;
Place p = {.extras = 0, .used = 0};
+ size_t where;
+ if (!at) at = &where;
if (!descend_key(list, cells[l->key].num, &p, at, exists)) return PURE_ALLOCATION;
if (*exists) return PURE_OK;
p.appending = *at == list->count;
@@ -1274,6 +1629,21 @@ static PureCell *log_cells(PureUndo *undo, unsigned fields) {
PureResult pure_list_remove(PureList *list, size_t at, PureUndo *undo) {
const Layout *l = &list->layout;
if (at >= list->count) return PURE_NOT_FOUND;
+ if (!logged(list, undo) || !positioned(list) ||
+ (list->form == FLAT && at + 1 < list->count && !flat_moves(list, at + 1) && !make_tree(list)))
+ return PURE_ALLOCATION;
+ if (list->form == FLAT) {
+ PureCell cells[16];
+ for (unsigned f = 0; f < l->fields; ++f) cells[f] = array_cell(list, at, f);
+ if (list->members) member_remove(list, cells[l->key].num);
+ list->bytes -= cells_bytes(l, cells);
+ let_go_cells(list, cells);
+ array_move(list, at, at + 1, list->count - at - 1);
+ list->moved += list->count - at - 1;
+ --list->count;
+ ++list->version;
+ return PURE_OK;
+ }
Step path[DEPTH];
unsigned within, depth = descend(list, at, path, &within);
if (!depth) return PURE_ALLOCATION;
@@ -1300,6 +1670,17 @@ PureResult pure_list_remove(PureList *list, size_t at, PureUndo *undo) {
PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell value, PureUndo *undo) {
const Layout *l = &list->layout;
if (at >= list->count) return PURE_NOT_FOUND;
+ if (!logged(list, undo) || !positioned(list)) return PURE_ALLOCATION;
+ if (list->form == FLAT) {
+ PureCell copy, old = array_cell(list, at, field);
+ if (!own_cell(list, field, value, &copy)) return PURE_ALLOCATION;
+ list->bytes += cell_bytes(l, field, copy) - cell_bytes(l, field, old);
+ array_put(list, at, field, copy);
+ let_go_text(list, old.text);
+ if ((int)field == l->key && list->members) member_remove(list, old.num), member_add(list, copy.num);
+ ++list->version;
+ return PURE_OK;
+ }
Step path[DEPTH];
unsigned within, depth = 0;
bool keyed = (int)field == l->key;
@@ -1343,6 +1724,10 @@ PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell val
}
PureCell pure_list_get(const PureList *list, size_t at, unsigned field) {
+ if (list->form != TREE) { /* a DENSE list by position: FLAT from now, or counted along its bits without memory */
+ if (list->form == DENSE && !positioned((PureList *)list)) return array_cell(list, dense_slot(list, at), field);
+ if (list->form == FLAT) return array_cell(list, at, field);
+ }
size_t within;
const Node *leaf = leaf_of(list, at, &within); /* first: an argument beside it would be read in either order */
return cell_at(&list->layout, leaf, (unsigned)within, field);
@@ -1356,8 +1741,29 @@ static void open_run(const Layout *l, const Node *leaf, PureRun *run) {
else run->column[f].numbers = numbers(l, leaf, f);
}
+/* A FLAT list's run from at: up to PURE_LIST_RUN items of its arrays; a
+ * DENSE one's (made FLAT without memory) its item at alone. */
+static void open_array_run(const PureList *list, size_t at, PureRun *run) {
+ const Layout *l = &list->layout;
+ size_t row = list->form == DENSE ? dense_slot(list, at) : at;
+ run->start = at;
+ run->count = list->form == DENSE ? 1 : (unsigned)(list->count - at < PURE_LIST_RUN ? list->count - at : PURE_LIST_RUN);
+ run->depth = 0;
+ for (unsigned f = 0; f < l->fields; ++f)
+ if (pure_cell_text(l->kinds[f])) run->column[f].texts = (const PureSpan *)list->columns[f] + row;
+ else if (l->kinds[f] == PURE_CELL_INTEGER) run->column[f].integers = (const int64_t *)list->columns[f] + row;
+ else run->column[f].numbers = (const double *)list->columns[f] + row;
+}
+
bool pure_list_run(const PureList *list, size_t at, PureRun *run) {
if (at >= list->count) return false;
+ if (list->form != TREE) {
+ if (list->form == DENSE) (void)positioned((PureList *)list);
+ if (list->form != TREE) {
+ open_array_run(list, at, run);
+ return true;
+ }
+ }
const Node *node = list->root;
size_t start = 0;
unsigned depth = 0;
@@ -1379,6 +1785,11 @@ bool pure_list_run(const PureList *list, size_t at, PureRun *run) {
* first children to a leaf. The leaf after it (a sibling) is fetched ahead:
* leaves copied at different times lie far apart in memory. */
bool pure_list_next(const PureList *list, PureRun *run) {
+ if (list->form != TREE) {
+ if (run->start + run->count >= list->count) return false;
+ open_array_run(list, run->start + run->count, run);
+ return true;
+ }
size_t start = run->start + run->count;
unsigned depth = run->depth;
while (depth && run->taken[depth - 1] + 1u >= ((const Node *)run->path[depth - 1])->count) --depth;
@@ -1417,10 +1828,34 @@ static int order(const Layout *l, unsigned field, PureCell a, PureCell b) {
* the children's first leaves. */
size_t pure_list_seek(const PureList *list, unsigned field, PureCell key, bool *found) {
const Layout *l = &list->layout;
+ *found = false;
+ if (!list->count) return 0;
+ if (list->form == DENSE && !positioned((PureList *)list) && (int)field == l->key) { /* counted along its bits */
+ size_t below = 0, all = list->member_capacity * 64; /* the keys below key: whole numbers below its ceiling */
+ size_t limit = key.num <= 0 ? 0 : key.num >= (double)all ? all : (size_t)ceil(key.num);
+ for (size_t w = 0; w < list->member_capacity && w * 64 < limit; ++w) {
+ uint64_t word = list->members[w];
+ if ((w + 1) * 64 > limit) word &= (UINT64_C(1) << (limit - w * 64)) - 1;
+ below += (size_t)__builtin_popcountll(word);
+ }
+ pure_list_contains(list, key.num, found);
+ return below;
+ }
+ if (list->form == FLAT) {
+ size_t low = 0, high = list->count;
+ if ((int)field == l->key) low = lower(list->columns[field], (unsigned)list->count, key.num);
+ else
+ while (low < high) {
+ size_t middle = low + (high - low) / 2;
+ if (order(l, field, array_cell(list, middle, field), key) < 0) low = middle + 1;
+ else high = middle;
+ }
+ *found = low < list->count && !order(l, field, array_cell(list, low, field), key);
+ return low;
+ }
const Node *node = list->root;
size_t start = 0;
- *found = false;
- if (!node || !list->count) return 0;
+ if (!node) return 0;
if ((int)field == l->key) {
/* The finger's leaf, when its keys reach round key, holds its place
* (a slice's high bound is near its low one, found just before). */
@@ -1497,6 +1932,7 @@ static bool gather_leaf(void *context, const Node *leaf) {
}
PURE_COLD PureResult pure_list_sorted(const PureList *list, unsigned field, PureList **out, size_t *moved) {
+ tree_now(list);
const Layout *l = &list->layout;
*out = NULL;
Gathered gathered = {list->count ? malloc(list->count * sizeof(Ref)) : NULL, 0};
diff --git a/core/module-list.h b/core/module-list.h
index 4778280..0addcf0 100644
--- a/core/module-list.h
+++ b/core/module-list.h
@@ -102,10 +102,11 @@ void pure_retired_free(PureRetired *);
* log means the change is not undone. */
PureResult pure_list_insert(PureList *, size_t at, const PureCell *cells, PureUndo *);
/* An item into a list kept in order of a number field, where its key goes,
- * found and placed in one descent (docs/design/020): at says where, exists
- * whether an item had the key (then nothing changes). Its change is logged
- * and journaled as pure_list_insert's. PURE_INVALID when the list is not
- * such a one (search it and insert then). */
+ * found and placed in one descent (docs/design/020): at says where (NULL:
+ * not asked, so a DENSE list need not count it), exists whether an item had
+ * the key (then nothing changes). Its change is logged and journaled as
+ * pure_list_insert's. PURE_INVALID when the list is not such a one (search
+ * it and insert then). */
PureResult pure_list_add(PureList *, const PureCell *cells, PureUndo *, size_t *at, bool *exists);
PureResult pure_list_remove(PureList *, size_t at, PureUndo *);
PureResult pure_list_set(PureList *, size_t at, unsigned field, PureCell value, PureUndo *);
@@ -134,6 +135,18 @@ bool pure_list_hashed(PureList *);
bool pure_list_contains(const PureList *, double key, bool *found);
/* The bytes it keeps beside its items for that. */
size_t pure_list_extra(const PureList *);
+/* An event's list (empty, never shared, journaled or undone) may keep its
+ * items in arrays rather than a tree (docs/design/021): in order (FLAT), or
+ * at their keys when keyed by small whole numbers (DENSE), as the changes it
+ * gets allow; a clear makes it FLAT again. Answers are the tree's. False
+ * when the list cannot (it stays a tree). */
+bool pure_list_arrays(PureList *);
+/* What it keeps its items in now: 0 a tree, 1 FLAT arrays, 2 DENSE (for tests and counts). */
+unsigned pure_list_form(const PureList *);
+/* Field of the item whose key_field is key, in a list ordered by key_field
+ * (seek, then get, as one: a DENSE list reads it at its key); false when no
+ * item has key. */
+bool pure_list_lookup(const PureList *, unsigned key_field, PureCell key, unsigned field, PureCell *cell);
/* The list's items ordered by field, as a keyed list keeps them: *out is NULL
* when they already are, or a new list in that order with moved[position
* before] = position after for every item. CONFLICT when two items have the
diff --git a/core/module.c b/core/module.c
index 2bf7ffd..c883c1f 100644
--- a/core/module.c
+++ b/core/module.c
@@ -354,6 +354,9 @@ 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;
}
+/* An item's or a key's op->plan: its cell a text; a FIND joined to the ITEM_GET after it. */
+enum { PLAN_TEXT = 1, PLAN_JOINED = 2 };
+
/* Whether a list's record field is a text. */
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;
@@ -1341,13 +1344,32 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
plan_folds(out, out->handler[i].code, input);
range_code(out, out->handler[i].code, input);
}
- /* What a cell is, read once here: an item's field, or a keyed list's key, a text (plan) or not. */
+ /* What a cell is, read once here: an item's field, or a keyed list's key, a text (PLAN_TEXT) or not. */
for (uint32_t pc = 0; pc < out->code_count; ++pc) {
PureOp *op = &out->code[pc];
if (op->op == PURE_OP_ITEM_GET || op->op == PURE_OP_ITEM_SET) op->plan = text_field(out, list_of(op->arg), item_field(op->arg));
else if (op->op == PURE_OP_LIST_FIND || op->op == PURE_OP_LIST_HAS)
op->plan = text_field(out, op->arg, out->state_key[op->arg] - 1u);
}
+ /* A FIND whose index the next instruction, an ITEM_GET of its list, takes
+ * at once (no jump lands between them) reads the field by key, as one
+ * (PLAN_JOINED, docs/design/021): no position is made. */
+ bool *landed = calloc(out->code_count + 1, sizeof(*landed));
+ if (!landed) {
+ result = PURE_ALLOCATION;
+ REFUSE("out of memory", 0);
+ }
+ for (uint32_t pc = 0; pc < out->code_count; ++pc)
+ 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])
+ op->plan |= PLAN_JOINED;
+ }
+ free(landed);
+ /* The lists an index state refers to: their inserts tell where (move_indices). */
+ for (uint32_t i = 0; i < out->states; ++i)
+ if (out->state_type[i] == PURE_STATE_INDEX) out->state_indexed[out->state_ref[i]] = true;
return PURE_OK;
refused:
if (error_size) snprintf(error, error_size, "%s (%u)", why, bad);
@@ -2682,7 +2704,9 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
status = PURE_INVALID;
if (m->state_key[arg]) { /* a keyed list places the item by its key: found and placed at once when kept by a number */
bool found;
- if ((status = pure_list_add(list, cells, undo_for(app, arg), &at, &found)) == PURE_INVALID) {
+ /* where it went matters to an index state's alone */
+ if ((status = pure_list_add(list, cells, undo_for(app, arg), m->state_indexed[arg] ? &at : NULL, &found)) ==
+ PURE_INVALID) {
unsigned key = m->state_key[arg] - 1u;
at = pure_list_seek(list, key, cells[key], &found);
}
@@ -2715,7 +2739,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
if (at >= pure_list_count(app->lists[list])) { kind = PURE_FAULT_NOT_FOUND; goto failed; }
PureCell cell = pure_list_get(app->lists[list], at, field);
/* A text is copied: the item may change or go before the value is used. */
- if (!op->plan) top->i64 = cell.i64; /* the bits: a number's, a bool's or an i64's (plan: a text, set at load) */
+ 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) return status;
continue;
}
@@ -2725,7 +2749,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
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 ? contiguous(app, arena, value.bytes, &span) : PURE_OK; /* plan: a text */
+ status = op->plan & PLAN_TEXT ? contiguous(app, arena, value.bytes, &span) : PURE_OK;
if (status == PURE_OK)
status = pure_list_set(app->lists[list], at, field,
(PureCell){.i64 = value.i64, .text = (unsigned char *)span.data, .size = span.size},
@@ -2861,12 +2885,23 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
unsigned key = m->state_key[arg] - 1u;
size_t mark = arena->used;
PureCell cell = {.i64 = top->i64};
- if (op->plan) { /* a text key (set at load) */
+ if (op->plan & PLAN_TEXT) { /* a text key (set at load) */
PureSpan span;
if ((status = contiguous(app, arena, top->bytes, &span)) != PURE_OK) return status;
cell = (PureCell){.text = (unsigned char *)span.data, .size = span.size};
}
bool found;
+ if (op->plan & PLAN_JOINED) { /* FIND, then ITEM_GET of what it found: the field by key */
+ const PureOp *get = &m->code[pc];
+ PureCell value;
+ found = pure_list_lookup(app->lists[arg], key, cell, item_field(get->arg), &value);
+ arena->used = mark;
+ if (!found) { kind = PURE_FAULT_NOT_FOUND; goto failed; }
+ ++pc;
+ if (!(get->plan & PLAN_TEXT)) top->i64 = value.i64;
+ else if ((status = keep_bytes(arena, value.text, value.size, &top->bytes)) != PURE_OK) return status;
+ continue;
+ }
/* An event's list keeps its keys as a set: HAS asks it alone (020). */
if (op->op == PURE_OP_LIST_HAS && pure_list_contains(app->lists[arg], cell.num, &found)) {
top->num = found;
@@ -3667,6 +3702,7 @@ static bool own_lists(PureApp *app, uint32_t lists) {
if (!(app->own[i] = new_list(m, m->state_ref[i]))) 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) */
}
app->lists[i] = app->own[i];
}
@@ -4335,6 +4371,8 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
(void)pure_list_keyed(lists[i], module->state_key[i] ? (int)module->state_key[i] - 1 : -1);
if (module->state_type[i] == PURE_STATE_LIST && module->state_key[i] && module->state_flags[i] & PURE_LIST_ROWS >> 8)
(void)pure_list_hashed(lists[i]);
+ if (module->state_type[i] == PURE_STATE_LIST && module->state_flags[i] & PURE_LIST_ROWS >> 8)
+ (void)pure_list_arrays(lists[i]); /* in arrays, not a tree, while it can (021) */
}
if (old) remap_outputs(app, old, module, true, NULL, NULL);
if (recast) { /* in the place of the outbox, its sends now the new module's */
diff --git a/core/module.h b/core/module.h
index 80d4460..9562358 100644
--- a/core/module.h
+++ b/core/module.h
@@ -254,6 +254,7 @@ typedef struct {
/* A keyed list: its key field + 1 (0: none). Its items stay in key order
* (numbers by value, texts by bytes), each key once, and keys do not change. */
uint8_t state_key[PURE_MODULE_STATES];
+ bool state_indexed[PURE_MODULE_STATES]; /* list: an index state refers to it (set at load) */
PureInputShape input[PURE_MODULE_INPUTS];
PureField field[PURE_MODULE_FIELDS];
PureValue initial[PURE_MODULE_STATES];
diff --git a/core/test-module.c b/core/test-module.c
index 3721c28..f76c871 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -1374,6 +1374,119 @@ static void keyed_journal(void) {
printf("{\"keyed_journal\":{\"adds\":%zu,\"had\":%zu,\"undone\":%zu,\"restarts\":%zu}}\n", adds, had, undone, restarts);
}
+/* An event's list in arrays (docs/design/021): beside a list that is a tree
+ * alone, the same random run of changes and questions gets the same answers
+ * at every step, whatever the arrays are then (FLAT, DENSE, or a tree after
+ * all): adds by key (where asked and not), inserts and removes anywhere,
+ * changes of a text and of a key, finds, reads by position and run by run,
+ * the field found by key, clears between events. Keys run small and whole
+ * (DENSE), past what DENSE may hold, and fractional (then the set is a
+ * table), so every change of form happens, back and forth. */
+static void same_list(const PureList *a, const PureList *t) {
+ CHECK(pure_list_count(a) == pure_list_count(t) && pure_list_size(a) == pure_list_size(t));
+ size_t n = pure_list_count(t);
+ for (size_t i = 0; i < n; i += 1 + i / 64) {
+ PureCell x = pure_list_get(a, i, 0), y = pure_list_get(t, i, 0), u = pure_list_get(a, i, 1), v = pure_list_get(t, i, 1);
+ CHECK(x.i64 == y.i64 && u.size == v.size && (!u.size || !memcmp(u.text, v.text, u.size)));
+ }
+}
+
+static void arrays_equivalence(void) {
+ static const uint8_t fields[2] = {PURE_CELL_NUMBER, PURE_CELL_TEXT};
+ uint32_t seed = 0x61727261u;
+ size_t forms[3] = {0}, conversions = 0, steps = 0;
+ for (unsigned keyed = 0; keyed < 2; ++keyed) {
+ PureList *a = pure_list_new(2, fields), *t = pure_list_new(2, fields);
+ CHECK(a && t);
+ if (keyed) {
+ CHECK(pure_list_keyed(a, 0) == PURE_OK && pure_list_keyed(t, 0) == PURE_OK);
+ CHECK(pure_list_hashed(a) && pure_list_hashed(t));
+ }
+ CHECK(pure_list_arrays(a) && pure_list_form(a) == 1);
+ unsigned form = pure_list_form(a);
+ for (unsigned event = 0; event < 400; ++event) {
+ /* Each event's keys: small and whole, sparse, or with fractions. */
+ unsigned kind = event % 5, changes = 50 + next_random(&seed) % (event % 7 == 0 ? 4000 : 400);
+ for (unsigned c = 0; c < changes; ++c, ++steps) {
+ uint32_t r = next_random(&seed);
+ size_t n = pure_list_count(t);
+ double key = kind < 3 ? (double)(r % 20000)
+ : kind == 3 ? (double)(r % 4000000)
+ : (double)(r % 80000) / 4;
+ char text[16];
+ size_t size = (size_t)snprintf(text, sizeof(text), "t%u", r % 1000);
+ PureCell cells[2] = {{.num = key}, {.text = (unsigned char *)text, .size = r % 11 ? size : 0}};
+ unsigned op = next_random(&seed) % 20;
+ if (keyed && event % 3 == 1) op = op < 12 ? 1 + op % 8 : 13 + op % 2; /* by key alone, as a search does: DENSE stays */
+ if (keyed && op < 9) { /* add by key, where it went asked or not */
+ bool ask = op == 0, ea, et;
+ size_t at_a = 0, at_t = 0;
+ CHECK(pure_list_add(a, cells, NULL, ask ? &at_a : NULL, &ea) == PURE_OK);
+ CHECK(pure_list_add(t, cells, NULL, &at_t, &et) == PURE_OK);
+ CHECK(ea == et && (!ask || at_a == at_t));
+ } else if (!keyed && op < 9) { /* insert anywhere: mostly the end */
+ size_t at = op < 6 ? n : n ? next_random(&seed) % (n + 1) : 0;
+ CHECK(pure_list_insert(a, at, cells, NULL) == PURE_OK && pure_list_insert(t, at, cells, NULL) == PURE_OK);
+ } else if (op < 11 && n) { /* remove */
+ size_t at = next_random(&seed) % n;
+ CHECK(pure_list_remove(a, at, NULL) == PURE_OK && pure_list_remove(t, at, NULL) == PURE_OK);
+ } else if (op < 12 && n) { /* a text changed; in a keyed list now and then its key, between its neighbours */
+ size_t at = next_random(&seed) % n;
+ CHECK(pure_list_set(a, at, 1, cells[1], NULL) == PURE_OK && pure_list_set(t, at, 1, cells[1], NULL) == PURE_OK);
+ if (keyed && at + 1 < n && r % 3 == 0) {
+ double low = pure_list_get(t, at, 0).num, high = pure_list_get(t, at + 1, 0).num, to = low + (high - low) / 2;
+ if (to > low && to < high) {
+ CHECK(pure_list_set(a, at, 0, (PureCell){.num = to}, NULL) == PURE_OK);
+ CHECK(pure_list_set(t, at, 0, (PureCell){.num = to}, NULL) == PURE_OK);
+ }
+ }
+ } else if (keyed && op < 15) { /* found, has, and its field by key */
+ bool fa, ft, ha, ht;
+ PureCell ca, ct;
+ if (op == 12) CHECK(pure_list_seek(a, 0, cells[0], &fa) == pure_list_seek(t, 0, cells[0], &ft) && fa == ft);
+ CHECK(pure_list_contains(a, key, &ha) && pure_list_contains(t, key, &ht) && ha == ht);
+ fa = pure_list_lookup(a, 0, cells[0], 1, &ca);
+ ft = pure_list_lookup(t, 0, cells[0], 1, &ct);
+ CHECK(fa == ft && fa == ht && (!fa || (ca.size == ct.size && (!ca.size || !memcmp(ca.text, ct.text, ca.size)))));
+ } else if (op < 17 && n) { /* by position */
+ size_t at = next_random(&seed) % n;
+ PureCell x = pure_list_get(a, at, 0), y = pure_list_get(t, at, 0);
+ CHECK(x.i64 == y.i64);
+ } else if (op < 18 && n) { /* run by run from somewhere */
+ size_t at = next_random(&seed) % n, seen = 0;
+ PureRun ra, rt;
+ CHECK(pure_list_run(a, at, &ra) && pure_list_run(t, at, &rt));
+ size_t ia = at, it = at;
+ for (bool ma = true, mt = true; seen < 200 && ma && mt; ++seen) {
+ while (ma && ia >= ra.start + ra.count) ma = pure_list_next(a, &ra);
+ while (mt && it >= rt.start + rt.count) mt = pure_list_next(t, &rt);
+ if (!ma || !mt) {
+ CHECK(ma == mt);
+ break;
+ }
+ CHECK(ra.column[0].numbers[ia - ra.start] == rt.column[0].numbers[it - rt.start]);
+ PureSpan sa = ra.column[1].texts[ia - ra.start], st = rt.column[1].texts[it - rt.start];
+ CHECK(sa.size == st.size && (!sa.size || !memcmp(sa.data, st.data, sa.size)));
+ ++ia, ++it;
+ }
+ }
+ if (pure_list_form(a) != form) ++conversions, form = pure_list_form(a);
+ ++forms[form];
+ }
+ same_list(a, t);
+ pure_list_clear(a); /* the event's end */
+ pure_list_clear(t);
+ CHECK(pure_list_count(a) == 0 && pure_list_form(a) == 1);
+ form = 1;
+ }
+ pure_list_free(a);
+ pure_list_free(t);
+ }
+ CHECK(forms[0] > 10000 && forms[1] > 10000 && forms[2] > 10000 && conversions > 100);
+ printf("{\"arrays\":{\"steps\":%zu,\"tree\":%zu,\"flat\":%zu,\"dense\":%zu,\"conversions\":%zu}}\n", steps, forms[0], forms[1],
+ forms[2], conversions);
+}
+
static void list_trees(void) {
static const uint8_t text_fields[2] = {0, 1};
uint32_t seed = 0x5eed1234u;
@@ -5850,6 +5963,7 @@ int main(void) {
list_trees();
key_sets();
keyed_journal();
+ arrays_equivalence();
folds();
filters();
lane_folds();
@@ -5878,6 +5992,6 @@ int main(void) {
stored_states();
deferred();
printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp));
- puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-bools-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-key-sets-text-order-message-fields-timers-now-snapshots-links\"}");
+ puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-bools-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-key-sets-arrays-text-order-message-fields-timers-now-snapshots-links\"}");
return 0;
}