aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/module-list.c570
-rw-r--r--core/module-list.h2
-rw-r--r--core/test-module.c210
3 files changed, 673 insertions, 109 deletions
diff --git a/core/module-list.c b/core/module-list.c
index 2cd2b1c..cfa9cf9 100644
--- a/core/module-list.c
+++ b/core/module-list.c
@@ -4,18 +4,43 @@
#include <stdlib.h>
#include <string.h>
+/* A list's items live in a persistent B+tree: leaves hold up to FANOUT
+ * items, inner nodes up to FANOUT children, and every node knows how many
+ * items lie below it, their stored bytes and the first of them. So an item
+ * is found by position, a keyed list is searched by key and an item's place
+ * in the stored form is computed in O(log n), whatever the list's length; a
+ * run of positions is read through a finger (the last leaf a thread read).
+ *
+ * Nodes, like items, carry the list's share count when they were made. One
+ * made before the last share may be read by a shared copy (a snapshot), so
+ * it never changes again: the list copies the nodes on the way to a change
+ * (the path) and retires the ones it replaced, as it does items. A share is
+ * then a root and a count, however long the list. */
+enum { FANOUT = 32, DEPTH = 16, SPARE_NODES = 8 };
+
+typedef struct Node {
+ uint64_t made; /* the list's share count when made: shared once below it */
+ uint32_t count; /* items (a leaf) or children */
+ bool leaf;
+ size_t items, bytes; /* items below, and their stored bytes */
+ PureCell *first; /* the first item below; NULL when there is none */
+ void *slot[FANOUT]; /* a leaf's items (PureCell *), an inner node's children (Node *) */
+} Node;
+
struct PureList {
unsigned fields;
uint8_t text_fields[16];
- PureCell **items;
- size_t count, capacity;
+ Node *root; /* NULL when no item was ever placed */
+ size_t count;
PureDraft *draft; /* the stored copy, or NULL */
size_t header; /* draft bytes before the first item */
uint64_t id, version; /* which list, and a number every change moves on */
- uint64_t shares; /* copies made so far: an item made before the last one is shared */
- PureRetired *retired; /* where shared items go when the list lets them go */
+ uint64_t shares; /* copies made so far: an item or node made before the last one is shared */
+ PureRetired *retired; /* where shared items and nodes go when the list lets them go */
+ Node *spare; /* nodes let go that no copy read, kept for the next ones (linked by slot[0]) */
+ unsigned spares;
unsigned refs; /* a shared copy's references */
- bool copy; /* a shared copy: it owns its item array, not its items */
+ bool copy; /* a shared copy: it owns none of its nodes or items */
};
typedef enum { INSERTED, REMOVED, CHANGED, REPLACED } Change;
@@ -27,10 +52,15 @@ typedef struct {
PureCell *item; /* REMOVED: the removed item; REPLACED: the shared item a copy replaced */
PureCell old; /* CHANGED: the replaced cell */
} Entry;
-typedef struct { PureCell *item; uint16_t text; uint8_t fields; } Retired;
+typedef struct { void *pointer; uint16_t text; uint8_t fields; bool node; } Retired;
static uint64_t lists_made;
+/* The leaf a thread read last, and where it starts: a list read position by
+ * position (a view's rows, a fold) finds each next item there. It holds for
+ * the list (its id) as it was (its version) and no other. */
+static _Thread_local struct { uint64_t id, version; const Node *leaf; size_t start; } finger;
+
/* An item's cells follow the list's share count when the item was made. One
* made before the last share may be read by a shared copy, so it never
* changes again (a change copies it) and is retired instead of freed. */
@@ -42,6 +72,9 @@ static PureCell *new_item(const PureList *list) {
}
static bool shared(const PureList *list, const PureCell *item) { return ((const uint64_t *)item)[-1] < list->shares; }
+static bool shared_node(const PureList *list, const Node *node) { return node->made < list->shares; }
+static Node *child(const Node *node, unsigned i) { return node->slot[i]; }
+static PureCell *item_in(const Node *leaf, unsigned i) { return leaf->slot[i]; }
static void free_cells(PureCell *item, unsigned fields, unsigned text) {
for (unsigned f = 0; f < fields; ++f)
@@ -59,9 +92,8 @@ static void free_item(const PureList *list, PureCell *item) {
if (item) free_cells(item, list->fields, text_mask(list));
}
-/* The list no longer has item: free it, or retire it while a copy may read it. */
-static void let_go(PureList *list, PureCell *item) {
- if (!shared(list, item)) { free_item(list, item); return; }
+/* Kept until the copies that may read it are gone (pure_retired_free). */
+static void retire(PureList *list, void *pointer, bool node) {
PureRetired *retired = list->retired;
if (retired->count == retired->capacity) {
size_t capacity = retired->capacity ? 2 * retired->capacity : 16;
@@ -70,16 +102,286 @@ static void let_go(PureList *list, PureCell *item) {
retired->entries = grown;
retired->capacity = capacity;
}
- ((Retired *)retired->entries)[retired->count++] = (Retired){item, (uint16_t)text_mask(list), (uint8_t)list->fields};
+ ((Retired *)retired->entries)[retired->count++] =
+ (Retired){pointer, node ? 0 : (uint16_t)text_mask(list), node ? 0 : (uint8_t)list->fields, node};
+}
+
+/* The list no longer has item: free it, or retire it while a copy may read it. */
+static void let_go(PureList *list, PureCell *item) {
+ if (!shared(list, item)) free_item(list, item);
+ else retire(list, item, false);
}
void pure_retired_free(PureRetired *retired) {
Retired *entries = retired->entries;
- for (size_t i = 0; i < retired->count; ++i) free_cells(entries[i].item, entries[i].fields, entries[i].text);
+ for (size_t i = 0; i < retired->count; ++i) {
+ if (entries[i].node) free(entries[i].pointer);
+ else free_cells(entries[i].pointer, entries[i].fields, entries[i].text);
+ }
free(entries);
*retired = (PureRetired){0};
}
+static Node *new_node(PureList *list, bool leaf) {
+ Node *node = list->spare;
+ if (node) {
+ list->spare = node->slot[0];
+ --list->spares;
+ } else if (!(node = malloc(sizeof(*node))))
+ return NULL;
+ node->made = list->shares;
+ node->count = 0;
+ node->leaf = leaf;
+ node->items = node->bytes = 0;
+ node->first = NULL;
+ return node;
+}
+
+/* A node the list no longer has (its items and children dealt with):
+ * retired while a copy may read it, else kept for the next or freed. */
+static void drop_node(PureList *list, Node *node) {
+ if (shared_node(list, node)) retire(list, node, true);
+ else if (list->spares < SPARE_NODES) {
+ node->slot[0] = list->spare;
+ list->spare = node;
+ ++list->spares;
+ } else
+ free(node);
+}
+
+/* ---- The stored form: each field in order, a number as its binary64 bits
+ * (little-endian), a text as a u32le length and its bytes. */
+
+static size_t cell_bytes(const PureList *list, unsigned field, PureCell cell) {
+ return list->text_fields[field] ? 4 + cell.size : 8;
+}
+
+static size_t item_bytes(const PureList *list, const PureCell *item) {
+ size_t size = 0;
+ for (unsigned f = 0; f < list->fields; ++f) size += cell_bytes(list, f, item[f]);
+ return size;
+}
+
+/* A node's summary, from its items or children. */
+static void sum(const PureList *list, Node *node) {
+ size_t items = 0, bytes = 0;
+ for (unsigned i = 0; i < node->count; ++i) {
+ if (node->leaf) ++items, bytes += item_bytes(list, item_in(node, i));
+ else items += child(node, i)->items, bytes += child(node, i)->bytes;
+ }
+ node->items = items;
+ node->bytes = bytes;
+ node->first = !node->count ? NULL : node->leaf ? item_in(node, 0) : child(node, 0)->first;
+}
+
+/* ---- Finding positions */
+
+typedef struct { Node *node; unsigned index; } Step; /* a node on the way, and the child taken */
+
+/* The leaf holding position at (the end is in the last leaf), and at's
+ * place in it. */
+static const Node *leaf_of(const PureList *list, size_t at, size_t *within) {
+ if (finger.leaf && finger.id == list->id && finger.version == list->version && at >= finger.start &&
+ at < finger.start + finger.leaf->count) {
+ *within = at - finger.start;
+ return finger.leaf;
+ }
+ const Node *node = list->root;
+ size_t start = 0;
+ while (!node->leaf) {
+ unsigned i = 0;
+ while (i + 1 < node->count && at - start >= child(node, i)->items) start += child(node, i)->items, ++i;
+ node = child(node, i);
+ }
+ finger.id = list->id, finger.version = list->version, finger.leaf = node, finger.start = start;
+ *within = at - start;
+ return node;
+}
+
+/* Where an item starts in the draft: the bytes of the children before it,
+ * level by level. */
+static size_t item_offset(const PureList *list, size_t at) {
+ size_t offset = list->header;
+ const Node *node = list->root;
+ if (!node) return offset;
+ while (!node->leaf) {
+ unsigned i = 0;
+ while (i + 1 < node->count && at >= child(node, i)->items) {
+ at -= child(node, i)->items;
+ offset += child(node, i)->bytes;
+ ++i;
+ }
+ node = child(node, i);
+ }
+ for (unsigned i = 0; i < at && i < node->count; ++i) offset += item_bytes(list, item_in(node, i));
+ return offset;
+}
+
+/* The node at *place, the list's own to change: a copy (the node retired)
+ * while a shared copy of the list may read it. NULL without memory. A copy
+ * moves the version on even when the change it is for does not happen: the
+ * list no longer has the nodes its shared copies have. */
+static Node *own(PureList *list, Node **place) {
+ Node *node = *place;
+ if (!shared_node(list, node)) return node;
+ Node *mine = new_node(list, node->leaf);
+ if (!mine) return NULL;
+ uint64_t made = mine->made;
+ memcpy(mine, node, sizeof(*mine));
+ mine->made = made;
+ retire(list, node, true);
+ *place = mine;
+ ++list->version;
+ return mine;
+}
+
+/* The path to the leaf holding position at, each node the list's own, and
+ * at's place in the leaf: the number of steps, 0 without memory (the list
+ * is as it was: copies replace what they copy). */
+static unsigned descend(PureList *list, size_t at, Step path[DEPTH], unsigned *within) {
+ Node **place = &list->root;
+ for (unsigned depth = 0; depth < DEPTH; ++depth) {
+ Node *node = own(list, place);
+ if (!node) return 0;
+ if (node->leaf) {
+ path[depth] = (Step){node, 0};
+ *within = (unsigned)at;
+ return depth + 1;
+ }
+ unsigned i = 0;
+ while (i + 1 < node->count && at >= child(node, i)->items) at -= child(node, i)->items, ++i;
+ path[depth] = (Step){node, i};
+ place = (Node **)&node->slot[i];
+ }
+ return 0;
+}
+
+static void sum_path(const PureList *list, const Step *path, unsigned depth) {
+ for (unsigned d = depth; d-- > 0;) sum(list, path[d].node);
+}
+
+/* ---- Placing and removing */
+
+/* A place for an item: the path to it, and the nodes a split may take (one
+ * a level and a root), made before anything changes so that placing cannot
+ * fail. */
+typedef struct {
+ Step path[DEPTH];
+ unsigned depth, within, extras, used;
+ Node *extra[DEPTH + 1];
+} Place;
+
+static bool prepare(PureList *list, size_t at, Place *p) {
+ if (!list->root && !(list->root = new_node(list, true))) return false;
+ p->extras = p->used = 0;
+ if (!(p->depth = descend(list, at, p->path, &p->within))) return false;
+ if (p->path[p->depth - 1].node->count < FANOUT) return true;
+ for (; p->extras <= p->depth; ++p->extras)
+ if (!(p->extra[p->extras] = new_node(list, false))) {
+ while (p->extras) drop_node(list, p->extra[--p->extras]);
+ return false;
+ }
+ return true;
+}
+
+static void release_extras(PureList *list, Place *p) {
+ while (p->extras > p->used) drop_node(list, p->extra[--p->extras]);
+}
+
+/* pointer into the node at the end of the path, before index: a full node
+ * splits in two halves and the new half goes into its parent, up to a new
+ * root. */
+static void place(PureList *list, Place *p, void *pointer) {
+ unsigned index = p->within;
+ for (unsigned d = p->depth; d-- > 0;) {
+ Node *node = p->path[d].node;
+ if (node->count < FANOUT) {
+ memmove(&node->slot[index + 1], &node->slot[index], (node->count - index) * sizeof(void *));
+ node->slot[index] = pointer;
+ ++node->count;
+ sum_path(list, p->path, d + 1);
+ break;
+ }
+ void *all[FANOUT + 1];
+ memcpy(all, node->slot, index * sizeof(void *));
+ all[index] = pointer;
+ memcpy(all + index + 1, node->slot + index, (FANOUT - index) * sizeof(void *));
+ Node *right = p->extra[p->used++];
+ right->leaf = node->leaf;
+ node->count = (FANOUT + 1) / 2;
+ right->count = FANOUT + 1 - node->count;
+ memcpy(node->slot, all, node->count * sizeof(void *));
+ memcpy(right->slot, all + node->count, right->count * sizeof(void *));
+ sum(list, node);
+ sum(list, right);
+ if (!d) { /* the root split: a new one above both halves */
+ Node *root = p->extra[p->used++];
+ root->leaf = false;
+ root->slot[0] = node;
+ root->slot[1] = right;
+ root->count = 2;
+ sum(list, root);
+ list->root = root;
+ break;
+ }
+ pointer = right;
+ index = p->path[d - 1].index + 1;
+ }
+ release_extras(list, p);
+ ++list->count;
+ ++list->version;
+}
+
+/* The item at the leaf's within, out of the tree (the caller keeps it): an
+ * emptied node leaves its parent, a node that fits with a neighbour merges
+ * into one (when the neighbour can be made the list's own), and a root with
+ * one child gives way to it. */
+static void take_out(PureList *list, Step *path, unsigned depth, unsigned within) {
+ Node *leaf = path[depth - 1].node;
+ memmove(&leaf->slot[within], &leaf->slot[within + 1], (leaf->count - within - 1) * sizeof(void *));
+ --leaf->count;
+ unsigned d = depth - 1;
+ for (;; --d) {
+ Node *node = path[d].node;
+ sum(list, node);
+ if (!d) break;
+ Node *parent = path[d - 1].node;
+ unsigned at = path[d - 1].index;
+ if (!node->count) {
+ memmove(&parent->slot[at], &parent->slot[at + 1], (parent->count - at - 1) * sizeof(void *));
+ --parent->count;
+ drop_node(list, node);
+ continue;
+ }
+ unsigned other = at + 1 < parent->count ? at + 1 : at ? at - 1 : at;
+ Node *neighbour = other != at && node->count + child(parent, other)->count <= FANOUT
+ ? own(list, (Node **)&parent->slot[other]) : NULL;
+ if (!neighbour) break;
+ Node *left = other < at ? neighbour : node, *right = other < at ? node : neighbour;
+ unsigned left_at = other < at ? other : at;
+ memcpy(&left->slot[left->count], right->slot, right->count * sizeof(void *));
+ left->count += right->count;
+ sum(list, left);
+ memmove(&parent->slot[left_at + 1], &parent->slot[left_at + 2], (parent->count - left_at - 2) * sizeof(void *));
+ --parent->count;
+ drop_node(list, right);
+ }
+ sum_path(list, path, d);
+ while (!list->root->leaf && list->root->count == 1) {
+ Node *old = list->root;
+ list->root = child(old, 0);
+ drop_node(list, old);
+ }
+ if (!list->root->count) { /* no item left */
+ drop_node(list, list->root);
+ list->root = NULL;
+ }
+ --list->count;
+ ++list->version;
+}
+
+/* ---- The list */
+
PureList *pure_list_new(unsigned fields, const uint8_t *text_fields) {
if (fields > 16) return NULL;
PureList *list = calloc(1, sizeof(*list));
@@ -90,13 +392,29 @@ PureList *pure_list_new(unsigned fields, const uint8_t *text_fields) {
return list;
}
+/* Every node and item below node let go (retired where a copy may read them). */
+static void let_go_tree(PureList *list, Node *node) {
+ if (!node) return;
+ for (unsigned i = 0; i < node->count; ++i) {
+ if (node->leaf) let_go(list, item_in(node, i));
+ else let_go_tree(list, child(node, i));
+ }
+ if (shared_node(list, node)) retire(list, node, true);
+ else free(node);
+}
+
void pure_list_free(PureList *list) {
if (!list) return;
if (list->copy) {
if (__atomic_sub_fetch(&list->refs, 1, __ATOMIC_ACQ_REL)) return;
- } else
- for (size_t i = 0; i < list->count; ++i) let_go(list, list->items[i]);
- free(list->items);
+ } else {
+ let_go_tree(list, list->root);
+ while (list->spare) {
+ Node *next = list->spare->slot[0];
+ free(list->spare);
+ list->spare = next;
+ }
+ }
free(list);
}
@@ -104,15 +422,9 @@ size_t pure_list_count(const PureList *list) { return list->count; }
PureList *pure_list_share(PureList *list, PureRetired *retired) {
PureList *copy = malloc(sizeof(*copy));
- PureCell **items = list->count ? malloc(list->count * sizeof(*items)) : NULL;
- if (!copy || (list->count && !items)) {
- free(copy);
- free(items);
- return NULL;
- }
- if (list->count) memcpy(items, list->items, list->count * sizeof(*items));
- *copy = (PureList){.fields = list->fields, .items = items, .count = list->count, .capacity = list->count,
- .id = list->id, .version = list->version, .refs = 1, .copy = true};
+ if (!copy) return NULL;
+ *copy = (PureList){.fields = list->fields, .root = list->root, .count = list->count, .id = list->id,
+ .version = list->version, .refs = 1, .copy = true};
memcpy(copy->text_fields, list->text_fields, sizeof(copy->text_fields));
list->retired = retired;
++list->shares;
@@ -147,26 +459,6 @@ static bool copy_cell(const PureList *list, unsigned field, PureCell from, PureC
return true;
}
-/* ---- The stored form: each field in order, a number as its binary64 bits
- * (little-endian), a text as a u32le length and its bytes. */
-
-static size_t cell_bytes(const PureList *list, unsigned field, PureCell cell) {
- return list->text_fields[field] ? 4 + cell.size : 8;
-}
-
-static size_t item_bytes(const PureList *list, const PureCell *item) {
- size_t size = 0;
- for (unsigned f = 0; f < list->fields; ++f) size += cell_bytes(list, f, item[f]);
- return size;
-}
-
-/* Where an item starts in the draft. Linear in the items before it. */
-static size_t item_offset(const PureList *list, size_t at) {
- size_t offset = list->header;
- for (size_t i = 0; i < at; ++i) offset += item_bytes(list, list->items[i]);
- return offset;
-}
-
static unsigned char *encode_cell(const PureList *list, unsigned field, PureCell cell, unsigned char *out) {
uint64_t bits = cell.size;
unsigned width = 4;
@@ -189,13 +481,27 @@ void pure_list_mirror(PureList *list, PureDraft *draft, size_t header) {
list->header = header;
}
+/* Every item in order, to visit (false stops it). */
+static bool visit_items(const Node *node, bool (*visit)(void *, PureCell *), void *context) {
+ for (unsigned i = 0; node && i < node->count; ++i)
+ if (node->leaf ? !visit(context, item_in(node, i)) : !visit_items(child(node, i), visit, context)) return false;
+ return true;
+}
+
+typedef struct { const PureList *list; unsigned char *at; } Encoding;
+
+static bool encode_item(void *context, PureCell *item) {
+ Encoding *e = context;
+ for (unsigned f = 0; f < e->list->fields; ++f) e->at = encode_cell(e->list, f, item[f], e->at);
+ return true;
+}
+
PURE_COLD PureResult pure_list_write(PureList *list) {
- size_t size = 0;
- for (size_t i = 0; i < list->count; ++i) size += item_bytes(list, list->items[i]);
- unsigned char *bytes = size ? malloc(size) : NULL, *at = bytes;
+ size_t size = list->root ? list->root->bytes : 0;
+ unsigned char *bytes = size ? malloc(size) : NULL;
if (size && !bytes) return PURE_ALLOCATION;
- for (size_t i = 0; i < list->count; ++i)
- for (unsigned f = 0; f < list->fields; ++f) at = encode_cell(list, f, list->items[i][f], at);
+ Encoding encoding = {list, bytes};
+ visit_items(list->root, encode_item, &encoding);
PureResult result = mirror(list, 0, 0, 0, bytes, size);
free(bytes);
return result;
@@ -227,39 +533,23 @@ PURE_COLD PureResult pure_list_read(PureList *list, const unsigned char *bytes,
return PURE_OK;
}
-static void place(PureList *list, size_t at, PureCell *item) {
- memmove(list->items + at + 1, list->items + at, (list->count - at) * sizeof(*list->items));
- list->items[at] = item;
- ++list->count;
- ++list->version;
-}
-
-static PureCell *take(PureList *list, size_t at) {
- PureCell *item = list->items[at];
- memmove(list->items + at, list->items + at + 1, (list->count - at - 1) * sizeof(*list->items));
- --list->count;
- ++list->version;
- return item;
-}
-
PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, PureUndo *undo) {
if (at > list->count || list->count == PURE_LIST_ITEMS) return PURE_LIMIT;
- if (list->count == list->capacity) {
- size_t capacity = list->capacity ? 2 * list->capacity : 8;
- PureCell **items = realloc(list->items, capacity * sizeof(*items));
- if (!items) return PURE_ALLOCATION;
- list->items = items;
- list->capacity = capacity;
- }
PureCell *item = new_item(list);
if (!item) return PURE_ALLOCATION;
for (unsigned f = 0; f < list->fields; ++f)
if (!copy_cell(list, f, cells[f], &item[f])) { free_item(list, item); return PURE_ALLOCATION; }
size_t size = list->draft ? item_bytes(list, item) : 0;
unsigned char *bytes = size ? malloc(size) : NULL, *end = bytes;
- if (size && !bytes) { free_item(list, item); return PURE_ALLOCATION; }
+ Place p;
+ if ((size && !bytes) || !prepare(list, at, &p)) {
+ free(bytes);
+ free_item(list, item);
+ return PURE_ALLOCATION;
+ }
for (unsigned f = 0; bytes && f < list->fields; ++f) end = encode_cell(list, f, item[f], end);
if (undo && !log_change(undo, (Entry){.change = INSERTED, .list = list, .at = at})) {
+ release_extras(list, &p);
free(bytes);
free_item(list, item);
return PURE_ALLOCATION;
@@ -268,20 +558,24 @@ PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, Pu
free(bytes);
if (result != PURE_OK) {
if (undo) --undo->count;
+ release_extras(list, &p);
free_item(list, item);
return result;
}
- place(list, at, item);
+ place(list, &p, item);
return PURE_OK;
}
PureResult pure_list_remove(PureList *list, size_t at, PureUndo *undo) {
if (at >= list->count) return PURE_NOT_FOUND;
- if (!log_change(undo, (Entry){.change = REMOVED, .list = list, .at = at, .item = list->items[at]}))
- return PURE_ALLOCATION;
- PureResult result = mirror(list, at, 0, item_bytes(list, list->items[at]), NULL, 0);
+ Step path[DEPTH];
+ unsigned within, depth = descend(list, at, path, &within);
+ if (!depth) return PURE_ALLOCATION;
+ PureCell *item = item_in(path[depth - 1].node, within);
+ if (!log_change(undo, (Entry){.change = REMOVED, .list = list, .at = at, .item = item})) return PURE_ALLOCATION;
+ PureResult result = mirror(list, at, 0, item_bytes(list, item), NULL, 0);
if (result != PURE_OK) { --undo->count; return result; }
- take(list, at);
+ take_out(list, path, depth, within);
return PURE_OK;
}
@@ -298,8 +592,12 @@ static PureCell *copy_item(const PureList *list, const PureCell *item, unsigned
PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell value, PureUndo *undo) {
if (at >= list->count) return PURE_NOT_FOUND;
- PureCell copy, *item = list->items[at];
- if (!copy_cell(list, field, value, &copy)) return PURE_ALLOCATION;
+ Step path[DEPTH];
+ unsigned within, depth = descend(list, at, path, &within);
+ PureCell copy;
+ if (!depth || !copy_cell(list, field, value, &copy)) return PURE_ALLOCATION;
+ Node *leaf = path[depth - 1].node;
+ PureCell *item = item_in(leaf, within);
PureCell *fresh = shared(list, item) ? copy_item(list, item, field) : item;
size_t size = list->draft ? cell_bytes(list, field, copy) : 0;
unsigned char *bytes = size ? malloc(size) : NULL;
@@ -312,9 +610,9 @@ PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell val
return PURE_ALLOCATION;
}
if (bytes) encode_cell(list, field, copy, bytes);
- size_t within = 0;
- for (unsigned f = 0; f < field; ++f) within += cell_bytes(list, f, item[f]);
- PureResult result = mirror(list, at, within, cell_bytes(list, field, item[field]), bytes, size);
+ size_t offset = 0;
+ for (unsigned f = 0; f < field; ++f) offset += cell_bytes(list, f, item[f]);
+ PureResult result = mirror(list, at, offset, cell_bytes(list, field, item[field]), bytes, size);
free(bytes);
if (result != PURE_OK) {
--undo->count;
@@ -323,13 +621,15 @@ PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell val
return result;
}
fresh[field] = copy;
- list->items[at] = fresh;
+ leaf->slot[within] = fresh;
+ sum_path(list, path, depth);
++list->version;
return PURE_OK;
}
PureCell pure_list_get(const PureList *list, size_t at, unsigned field) {
- return list->items[at][field];
+ size_t within;
+ return item_in(leaf_of(list, at, &within), (unsigned)within)[field];
}
/* How two cells of a field order: numbers by value (-0 and +0 are one key),
@@ -341,50 +641,85 @@ static int order(const PureList *list, unsigned field, PureCell a, PureCell b) {
return bytes ? (bytes > 0) - (bytes < 0) : (a.size > b.size) - (a.size < b.size);
}
+/* Down the child whose first item's key is the last at or below key (the
+ * first child when none is), then the leaf's first item not below it. */
size_t pure_list_seek(const PureList *list, unsigned field, PureCell key, bool *found) {
- size_t low = 0, high = list->count;
+ const Node *node = list->root;
+ size_t start = 0;
+ *found = false;
+ if (!node || !list->count) return 0;
+ while (!node->leaf) {
+ unsigned low = 1, high = node->count;
+ while (low < high) {
+ unsigned middle = low + (high - low) / 2;
+ if (order(list, field, child(node, middle)->first[field], key) <= 0) low = middle + 1;
+ else high = middle;
+ }
+ for (unsigned i = 0; i + 1 < low; ++i) start += child(node, i)->items;
+ node = child(node, low - 1);
+ }
+ unsigned low = 0, high = node->count;
while (low < high) {
- size_t middle = low + (high - low) / 2;
- if (order(list, field, list->items[middle][field], key) < 0) low = middle + 1;
+ unsigned middle = low + (high - low) / 2;
+ if (order(list, field, item_in(node, middle)[field], key) < 0) low = middle + 1;
else high = middle;
}
- *found = low < list->count && !order(list, field, list->items[low][field], key);
- return low;
+ *found = low < node->count && !order(list, field, item_in(node, low)[field], key);
+ return start + low;
}
/* Positions sorted by their items' field, stably (spare holds count). */
-PURE_COLD static void merge_sort(const PureList *list, unsigned field, size_t *at, size_t *spare, size_t count) {
+PURE_COLD static void merge_sort(const PureList *list, unsigned field, PureCell *const *items, size_t *at, size_t *spare,
+ size_t count) {
if (count < 2) return;
size_t half = count / 2, i = 0, j = half, k = 0;
- merge_sort(list, field, at, spare, half);
- merge_sort(list, field, at + half, spare, count - half);
+ merge_sort(list, field, items, at, spare, half);
+ merge_sort(list, field, items, at + half, spare, count - half);
while (i < half && j < count)
- spare[k++] = order(list, field, list->items[at[j]][field], list->items[at[i]][field]) < 0 ? at[j++] : at[i++];
+ spare[k++] = order(list, field, items[at[j]][field], items[at[i]][field]) < 0 ? at[j++] : at[i++];
while (i < half) spare[k++] = at[i++];
while (j < count) spare[k++] = at[j++];
memcpy(at, spare, count * sizeof(*at));
}
+typedef struct { PureCell **items; size_t count; } Gathered;
+
+static bool gather_item(void *context, PureCell *item) {
+ Gathered *g = context;
+ g->items[g->count++] = item;
+ return true;
+}
+
PURE_COLD PureResult pure_list_sorted(const PureList *list, unsigned field, PureList **out, size_t *moved) {
*out = NULL;
+ Gathered gathered = {list->count ? malloc(list->count * sizeof(PureCell *)) : NULL, 0};
+ if (list->count && !gathered.items) return PURE_ALLOCATION;
+ visit_items(list->root, gather_item, &gathered);
+ PureCell **items = gathered.items;
bool ordered = true;
- for (size_t i = 1; i < list->count && ordered; ++i)
- ordered = order(list, field, list->items[i - 1][field], list->items[i][field]) < 0;
- if (ordered) return PURE_OK;
+ for (size_t i = 1; i < list->count && ordered; ++i) ordered = order(list, field, items[i - 1][field], items[i][field]) < 0;
+ if (ordered) {
+ free(items);
+ return PURE_OK;
+ }
size_t *at = malloc(2 * list->count * sizeof(*at));
- if (!at) return PURE_ALLOCATION;
+ if (!at) {
+ free(items);
+ return PURE_ALLOCATION;
+ }
for (size_t i = 0; i < list->count; ++i) at[i] = i;
- merge_sort(list, field, at, at + list->count, list->count);
+ merge_sort(list, field, items, at, at + list->count, list->count);
PureResult result = PURE_OK;
for (size_t i = 1; i < list->count && result == PURE_OK; ++i)
- if (!order(list, field, list->items[at[i - 1]][field], list->items[at[i]][field])) result = PURE_CONFLICT;
+ if (!order(list, field, items[at[i - 1]][field], items[at[i]][field])) result = PURE_CONFLICT;
PureList *sorted = result == PURE_OK ? pure_list_new(list->fields, list->text_fields) : NULL;
if (result == PURE_OK && !sorted) result = PURE_ALLOCATION;
for (size_t i = 0; result == PURE_OK && i < list->count; ++i) {
- result = pure_list_insert(sorted, i, list->items[at[i]], NULL);
+ result = pure_list_insert(sorted, i, items[at[i]], NULL);
moved[at[i]] = i;
}
free(at);
+ free(items);
if (result != PURE_OK) {
pure_list_free(sorted);
return result;
@@ -393,21 +728,40 @@ PURE_COLD PureResult pure_list_sorted(const PureList *list, unsigned field, Pure
return PURE_OK;
}
+/* Undoing needs no memory but for a node an item goes back into (the event
+ * took one out and the tree was since reshaped): without it, the process
+ * stops rather than keep half an event (the store keeps the last save). */
void pure_undo_rollback(PureUndo *undo, size_t mark) {
Entry *entries = undo->entries;
for (size_t i = undo->count; i-- > mark;) {
Entry *e = &entries[i];
- if (e->change == INSERTED) free_item(e->list, take(e->list, e->at));
- else if (e->change == REMOVED) place(e->list, e->at, e->item); /* capacity is still there */
- else if (e->change == REPLACED) {
- free_item(e->list, e->list->items[e->at]);
- e->list->items[e->at] = e->item;
- ++e->list->version;
+ PureList *list = e->list;
+ Step path[DEPTH];
+ unsigned within, depth;
+ if (e->change == REMOVED) {
+ Place p;
+ if (!prepare(list, e->at, &p)) abort();
+ place(list, &p, e->item);
+ continue;
+ }
+ if (!(depth = descend(list, e->at, path, &within))) abort();
+ Node *leaf = path[depth - 1].node;
+ if (e->change == INSERTED) {
+ PureCell *item = item_in(leaf, within);
+ take_out(list, path, depth, within);
+ free_item(list, item);
+ continue;
+ }
+ PureCell *item = item_in(leaf, within);
+ if (e->change == REPLACED) {
+ free_item(list, item);
+ leaf->slot[within] = e->item;
} else {
- if (e->list->text_fields[e->field]) free(e->list->items[e->at][e->field].text);
- e->list->items[e->at][e->field] = e->old;
- ++e->list->version;
+ if (list->text_fields[e->field]) free(item[e->field].text);
+ item[e->field] = e->old;
}
+ sum_path(list, path, depth);
+ ++list->version;
}
undo->count = mark;
}
diff --git a/core/module-list.h b/core/module-list.h
index a721ff0..38eb0f7 100644
--- a/core/module-list.h
+++ b/core/module-list.h
@@ -25,7 +25,7 @@ typedef struct { void *entries; size_t count, capacity; } PureUndo;
/* Items a list let go while a shared copy may still read them. */
typedef struct { void *entries; size_t count, capacity; } PureRetired;
-enum { PURE_LIST_ITEMS = 65536 };
+enum { PURE_LIST_ITEMS = 1 << 24 };
PureList *pure_list_new(unsigned fields, const uint8_t *text_fields);
void pure_list_free(PureList *);
diff --git a/core/test-module.c b/core/test-module.c
index 307d565..e6df3ad 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -920,6 +920,215 @@ static const char *item_title(const PureApp *app, uint32_t list, size_t at) {
/* A task list: made (num), tasks (list of task {title, done}), at (index of
* tasks). Indices follow their item across inserts and removes; a failed
* event undoes every list change. */
+/* ---- Lists as persistent trees (module-list.c): a model array beside a list
+ * of thousands of items, changed by events that are committed or undone at
+ * random, with a stored copy (a draft) kept byte for byte, shares (a
+ * snapshot's copies) that keep what they shared while the list changes on,
+ * and a keyed list searched by key. */
+
+typedef struct { double key; char text[12]; size_t size; } ModelItem;
+typedef struct { ModelItem *items; size_t count, capacity; } Model;
+
+static void model_insert(Model *m, size_t at, ModelItem item) {
+ if (m->count == m->capacity) {
+ m->capacity = m->capacity ? 2 * m->capacity : 64;
+ m->items = realloc(m->items, m->capacity * sizeof(*m->items));
+ CHECK(m->items);
+ }
+ memmove(m->items + at + 1, m->items + at, (m->count - at) * sizeof(*m->items));
+ m->items[at] = item;
+ ++m->count;
+}
+
+static void model_copy(Model *to, const Model *from) {
+ to->count = from->count;
+ if (to->capacity < from->count) {
+ to->capacity = from->capacity;
+ to->items = realloc(to->items, to->capacity * sizeof(*to->items));
+ CHECK(to->items);
+ }
+ if (from->count) memcpy(to->items, from->items, from->count * sizeof(*to->items));
+}
+
+static bool same_items(const PureList *list, const Model *m) {
+ if (pure_list_count(list) != m->count) return false;
+ for (size_t i = 0; i < m->count; ++i) {
+ PureCell key = pure_list_get(list, i, 0), text = pure_list_get(list, i, 1);
+ if (key.num != m->items[i].key || text.size != m->items[i].size ||
+ (text.size && memcmp(text.text, m->items[i].text, text.size)))
+ return false;
+ }
+ return true;
+}
+
+typedef struct { unsigned char *bytes; size_t size, capacity; } Grown;
+
+static bool collect_bytes(void *context, PureSpan span) {
+ Grown *b = context;
+ if (b->size + span.size > b->capacity) {
+ b->capacity = 2 * (b->size + span.size);
+ b->bytes = realloc(b->bytes, b->capacity);
+ CHECK(b->bytes);
+ }
+ memcpy(b->bytes + b->size, span.data, span.size);
+ b->size += span.size;
+ return true;
+}
+
+/* The stored form of the model: a number's binary64 bits, a text's u32le length and bytes. */
+static bool stored_as(PureDraft *draft, const Model *m) {
+ Grown got = {0};
+ CHECK(pure_draft_read(draft, 0, pure_draft_size(draft), collect_bytes, &got) == PURE_OK);
+ size_t at = 0;
+ bool same = true;
+ for (size_t i = 0; i < m->count && same; ++i) {
+ uint64_t bits, size = 0;
+ memcpy(&bits, &m->items[i].key, 8);
+ for (unsigned k = 0; k < 8; ++k) same = same && at + k < got.size && got.bytes[at + k] == (unsigned char)(bits >> (8 * k));
+ at += 8;
+ for (unsigned k = 0; k < 4 && at + k < got.size; ++k) size |= (uint64_t)got.bytes[at + k] << (8 * k);
+ at += 4;
+ same = same && size == m->items[i].size && at + size <= got.size && !memcmp(got.bytes + at, m->items[i].text, size);
+ at += size;
+ }
+ same = same && at == got.size;
+ free(got.bytes);
+ return same;
+}
+
+static void list_trees(void) {
+ static const uint8_t text_fields[2] = {0, 1};
+ uint32_t seed = 0x5eed1234u;
+ PureList *list = pure_list_new(2, text_fields);
+ PureDraft *draft = pure_draft_new();
+ CHECK(list && draft);
+ pure_list_mirror(list, draft, 0);
+ PureUndo undo = {0};
+ Model model = {0}, before = {0};
+ /* Up to four shares at a time, each with what it shared and the items
+ * and nodes retired while it was the newest. */
+ enum { SHARES = 4 };
+ PureList *copies[SHARES];
+ Model shared[SHARES] = {{0}};
+ PureRetired retired[SHARES + 1] = {{0}};
+ unsigned first = 0, held = 0, splits_seen = 0;
+ size_t largest = 0, events = 0, undone = 0;
+ for (unsigned event = 0; event < 6000; ++event) {
+ size_t mark = undo.count;
+ model_copy(&before, &model);
+ pure_draft_hold(draft, true);
+ PureVersion version = pure_draft_version(draft);
+ /* Mostly growing to a few thousand, then shrinking back down, so the
+ * tree gains and loses levels. */
+ unsigned grow = event < 2500 ? 70 : event < 4500 ? 30 : 60;
+ for (unsigned change = 1 + (unsigned)(next_random(&seed) % 12); change--;) {
+ unsigned roll = (unsigned)(next_random(&seed) % 100);
+ if (roll < grow || !model.count) {
+ size_t at = model.count ? next_random(&seed) % (model.count + 1) : 0;
+ ModelItem item = {(double)(next_random(&seed) % 100000) - 50000, "", next_random(&seed) % 11};
+ for (size_t k = 0; k < item.size; ++k) item.text[k] = (char)('a' + next_random(&seed) % 26);
+ PureCell cells[2] = {{item.key, NULL, 0}, {0, (unsigned char *)item.text, item.size}};
+ CHECK(pure_list_insert(list, at, cells, &undo) == PURE_OK);
+ model_insert(&model, at, item);
+ } else if (roll < grow + 25) {
+ size_t at = next_random(&seed) % model.count;
+ CHECK(pure_list_remove(list, at, &undo) == PURE_OK);
+ memmove(model.items + at, model.items + at + 1, (model.count - at - 1) * sizeof(*model.items));
+ --model.count;
+ } else {
+ size_t at = next_random(&seed) % model.count;
+ if (next_random(&seed) % 2) {
+ double key = (double)(next_random(&seed) % 1000);
+ CHECK(pure_list_set(list, at, 0, (PureCell){key, NULL, 0}, &undo) == PURE_OK);
+ model.items[at].key = key;
+ } else {
+ ModelItem *item = &model.items[at];
+ item->size = next_random(&seed) % 11;
+ for (size_t k = 0; k < item->size; ++k) item->text[k] = (char)('A' + next_random(&seed) % 26);
+ CHECK(pure_list_set(list, at, 1, (PureCell){0, (unsigned char *)item->text, item->size}, &undo) == PURE_OK);
+ }
+ }
+ }
+ if (next_random(&seed) % 5 == 0) { /* undone: the list and its stored copy as before */
+ pure_undo_rollback(&undo, mark);
+ pure_draft_restore(draft, version);
+ model_copy(&model, &before);
+ ++undone;
+ } else
+ pure_undo_commit(&undo);
+ pure_draft_hold(draft, false);
+ ++events;
+ largest = model.count > largest ? model.count : largest;
+ splits_seen += model.count > 32;
+ if (event % 7 == 0) CHECK(same_items(list, &model));
+ if (event % 50 == 0) CHECK(stored_as(draft, &model));
+ /* A share now and then, as a snapshot takes one; the oldest goes with
+ * the items and nodes retired while it was the newest. */
+ if (event % 40 == 20) {
+ if (held == SHARES) {
+ CHECK(same_items(copies[first], &shared[first]));
+ pure_list_free(copies[first]);
+ pure_retired_free(&retired[first]);
+ first = (first + 1) % SHARES;
+ --held;
+ }
+ unsigned slot = (first + held) % SHARES;
+ CHECK((copies[slot] = pure_list_share(list, &retired[slot])));
+ model_copy(&shared[slot], &model);
+ ++held;
+ }
+ if (event % 25 == 0)
+ for (unsigned i = 0; i < held; ++i) CHECK(same_items(copies[(first + i) % SHARES], &shared[(first + i) % SHARES]));
+ }
+ CHECK(same_items(list, &model) && stored_as(draft, &model) && largest > 2000 && undone > 500);
+ for (; held; --held, first = (first + 1) % SHARES) {
+ CHECK(same_items(copies[first], &shared[first]));
+ pure_list_free(copies[first]);
+ pure_retired_free(&retired[first]);
+ }
+ pure_list_free(list);
+ pure_draft_close(draft);
+ pure_undo_free(&undo);
+
+ /* A keyed list: items kept in key order, found by key. */
+ PureList *keyed = pure_list_new(2, text_fields);
+ Model keys = {0};
+ CHECK(keyed);
+ for (unsigned round = 0; round < 40000; ++round) {
+ double key = (double)(next_random(&seed) % 20000);
+ bool found;
+ size_t at = pure_list_seek(keyed, 0, (PureCell){key, NULL, 0}, &found);
+ size_t low = 0, high = keys.count;
+ while (low < high) {
+ size_t middle = (low + high) / 2;
+ if (keys.items[middle].key < key) low = middle + 1;
+ else high = middle;
+ }
+ CHECK(at == low && found == (low < keys.count && keys.items[low].key == key));
+ if (!found) {
+ PureCell cells[2] = {{key, NULL, 0}, {0, (unsigned char *)"k", 1}};
+ CHECK(pure_list_insert(keyed, at, cells, NULL) == PURE_OK);
+ model_insert(&keys, at, (ModelItem){key, "k", 1});
+ } else if (next_random(&seed) % 3 == 0) {
+ PureUndo removal = {0};
+ CHECK(pure_list_remove(keyed, at, &removal) == PURE_OK);
+ pure_undo_commit(&removal);
+ pure_undo_free(&removal);
+ memmove(keys.items + at, keys.items + at + 1, (keys.count - at - 1) * sizeof(*keys.items));
+ --keys.count;
+ }
+ }
+ CHECK(same_items(keyed, &keys) && keys.count > 5000);
+ pure_list_free(keyed);
+ free(model.items);
+ free(before.items);
+ free(keys.items);
+ for (unsigned i = 0; i < SHARES; ++i) free(shared[i].items);
+ for (unsigned i = 0; i <= SHARES; ++i) pure_retired_free(&retired[i]);
+ printf("{\"list_tree_events\":%zu,\"undone\":%zu,\"largest\":%zu,\"keyed_items\":%zu}\n", events, undone,
+ largest, keys.count);
+}
+
static void lists(void) {
static const S nums[] = {{"made", 0}};
static const T states[] = {{"tasks", 3, 0}, {"at", 4, 1}};
@@ -2867,6 +3076,7 @@ int main(void) {
marks();
text_mutations();
lists();
+ list_trees();
folds();
filters();
keyed();