From 18675f3e45fa0896efd777bf66ad0ec4c8157d36 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Fri, 25 Sep 2026 15:49:48 +0100 Subject: Keep app states in the tree store When the data host keeps states (PureData.state and .save, implemented by pure_tree_data), every state of an app lives in the store: a text state's draft is its stored value, a list mirrors each change into a stored draft (record header, then items), and numbers, cursors and indices are stored together by name. An app starts from what is stored, moves list fields by name when a record changed, resets cursors and indices that no longer fit, and refuses damaged stored values. Each key, text and message event publishes the states it changed together with the message's document in one store commit, or fails and leaves memory and store as they were. Frame changes go with the next event or pure_app_save. pure-run takes --store PATH. Tests run the same random events on an app in memory and on a stored app that restarts every few events and require equal states after every step; kill a process at every commit stage of an event that changes a number, a text and a list; and check frames, message atomicity and migration by restart and by replacement. A stored event costs about 10.6 ms more on the Pi's SD card, mostly two fsyncs. --- prototypes/connections/Makefile | 12 +- prototypes/connections/module-list.c | 117 ++++++++++++- prototypes/connections/module-list.h | 11 ++ prototypes/connections/module-save.c | 291 +++++++++++++++++++++++++++++++ prototypes/connections/module-save.h | 30 ++++ prototypes/connections/module-store.c | 21 ++- prototypes/connections/module-store.h | 5 +- prototypes/connections/module.c | 151 ++++++++++++---- prototypes/connections/module.h | 18 +- prototypes/connections/pure-run.c | 21 ++- prototypes/connections/test-module.c | 317 +++++++++++++++++++++++++++++++++- 11 files changed, 937 insertions(+), 57 deletions(-) create mode 100644 prototypes/connections/module-save.c create mode 100644 prototypes/connections/module-save.h (limited to 'prototypes/connections') diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile index 9061bf8..e78fdbd 100644 --- a/prototypes/connections/Makefile +++ b/prototypes/connections/Makefile @@ -117,14 +117,14 @@ $(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUI # Pure modules: loader/verifier/executor, its tests and a test host. # Text states are runtime-owned drafts, so the executor links the tree engine. -MODULE = module.c module-list.c module-text.c store-tree.c -MODULE_HEADERS = module.h module-list.h module-text.h $(HEADERS) +MODULE = module.c module-list.c module-save.c module-text.c store-tree.c +MODULE_HEADERS = module.h module-list.h module-save.h module-text.h $(HEADERS) -$(BUILD)/test-module: test-module.c $(MODULE) $(MODULE_HEADERS) | $(BUILD) - $(CC) $(CPPFLAGS) $(CFLAGS) test-module.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@ +$(BUILD)/test-module: test-module.c module-store.c $(MODULE) module-store.h $(MODULE_HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-module.c module-store.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@ -$(BUILD)/pure-run: pure-run.c $(MODULE) $(MODULE_HEADERS) | $(BUILD) - $(CC) $(CPPFLAGS) $(CFLAGS) pure-run.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@ +$(BUILD)/pure-run: pure-run.c module-store.c $(MODULE) module-store.h $(MODULE_HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) pure-run.c module-store.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@ # The document CLI contract driven by a module on the tree store. $(BUILD)/pure-edit-module: edit-module.c module-store.c $(MODULE) module-store.h $(MODULE_HEADERS) | $(BUILD) diff --git a/prototypes/connections/module-list.c b/prototypes/connections/module-list.c index 68380f2..ba67143 100644 --- a/prototypes/connections/module-list.c +++ b/prototypes/connections/module-list.c @@ -1,4 +1,6 @@ #include "module-list.h" +#include "module-text.h" +#include #include #include @@ -7,6 +9,8 @@ struct PureList { uint8_t text_fields[16]; PureCell **items; size_t count, capacity; + PureDraft *draft; /* the stored copy, or NULL */ + size_t header; /* draft bytes before the first item */ }; typedef enum { INSERTED, REMOVED, CHANGED } Change; @@ -66,6 +70,84 @@ 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; + if (!list->text_fields[field]) { memcpy(&bits, &cell.num, sizeof(bits)); width = 8; } + for (unsigned i = 0; i < width; ++i) *out++ = (unsigned char)(bits >> (8 * i)); + if (list->text_fields[field] && cell.size) { memcpy(out, cell.text, cell.size); out += cell.size; } + return out; +} + +/* Replace [offset, offset + remove) of the draft; nothing to do without one. */ +static PureResult mirror(PureList *list, size_t offset, size_t remove, const unsigned char *bytes, size_t size) { + if (!list->draft) return PURE_OK; + return pure_draft_splice(list->draft, offset, remove, (PureSpan){bytes, size}); +} + +void pure_list_mirror(PureList *list, PureDraft *draft, size_t header) { + list->draft = draft; + list->header = header; +} + +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; + 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); + PureResult result = mirror(list, list->header, 0, bytes, size); + free(bytes); + return result; +} + +PureResult pure_list_read(PureList *list, const unsigned char *bytes, size_t size) { + PureCell cells[16]; + for (size_t at = 0; at < size;) { + for (unsigned f = 0; f < list->fields; ++f) { + size_t width = list->text_fields[f] ? 4 : 8; + if (size - at < width) return PURE_STORE_ERROR; + uint64_t bits = 0; + for (size_t i = 0; i < width; ++i) bits |= (uint64_t)bytes[at + i] << (8 * i); + at += width; + if (!list->text_fields[f]) { + cells[f] = (PureCell){0}; + memcpy(&cells[f].num, &bits, sizeof(bits)); + if (!isfinite(cells[f].num)) return PURE_STORE_ERROR; + continue; + } + if (bits > size - at || !pure_utf8_valid((PureSpan){bytes + at, (size_t)bits})) + return PURE_STORE_ERROR; + cells[f] = (PureCell){0, (unsigned char *)bytes + at, (size_t)bits}; + at += (size_t)bits; + } + PureResult result = pure_list_insert(list, list->count, cells, NULL); + if (result != PURE_OK) return result; + } + 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; @@ -92,10 +174,22 @@ PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, Pu 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; } + 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})) { + free(bytes); free_item(list, item); return PURE_ALLOCATION; } + PureResult result = mirror(list, item_offset(list, at), 0, bytes, size); + free(bytes); + if (result != PURE_OK) { + if (undo) --undo->count; + free_item(list, item); + return result; + } place(list, at, item); return PURE_OK; } @@ -104,20 +198,35 @@ 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, item_offset(list, at), item_bytes(list, list->items[at]), NULL, 0); + if (result != PURE_OK) { --undo->count; return result; } take(list, at); return PURE_OK; } 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; + PureCell copy, *item = list->items[at]; if (!copy_cell(list, field, value, ©)) return PURE_ALLOCATION; - if (!log_change(undo, (Entry){.change = CHANGED, .list = list, .at = at, .field = field, - .old = list->items[at][field]})) { + size_t size = list->draft ? cell_bytes(list, field, copy) : 0; + unsigned char *bytes = size ? malloc(size) : NULL; + if (size && !bytes) { free(copy.text); return PURE_ALLOCATION; } + if (bytes) encode_cell(list, field, copy, bytes); + if (!log_change(undo, (Entry){.change = CHANGED, .list = list, .at = at, .field = field, .old = item[field]})) { + free(bytes); free(copy.text); return PURE_ALLOCATION; } - list->items[at][field] = copy; + size_t offset = list->draft ? item_offset(list, at) : 0; + for (unsigned f = 0; list->draft && f < field; ++f) offset += cell_bytes(list, f, item[f]); + PureResult result = mirror(list, offset, cell_bytes(list, field, item[field]), bytes, size); + free(bytes); + if (result != PURE_OK) { + --undo->count; + free(copy.text); + return result; + } + item[field] = copy; return PURE_OK; } diff --git a/prototypes/connections/module-list.h b/prototypes/connections/module-list.h index f896e33..e79ee46 100644 --- a/prototypes/connections/module-list.h +++ b/prototypes/connections/module-list.h @@ -1,6 +1,7 @@ #ifndef PURE_MODULE_LIST_H #define PURE_MODULE_LIST_H #include "result.h" +#include "state.h" #include #include #include @@ -24,6 +25,16 @@ PureResult pure_list_insert(PureList *, size_t at, const PureCell *cells, PureUn PureResult pure_list_remove(PureList *, size_t at, PureUndo *); PureResult pure_list_set(PureList *, size_t at, unsigned field, PureCell value, PureUndo *); PureCell pure_list_get(const PureList *, size_t at, unsigned field); +/* A list may keep a stored copy in a draft: header bytes, then each item's + * fields in order (a number as its binary64 bits, little-endian; a text as a + * u32le length and its bytes). Every change splices the draft as well, and a + * change the draft refuses (such as its size limit) does not happen. Undoing + * an event changes only the list: the caller restores the draft's version. */ +void pure_list_mirror(PureList *, PureDraft *, size_t header); +/* Store every item after the header (the draft holds just the header). */ +PureResult pure_list_write(PureList *); +/* Append the items stored in bytes; each field is checked. Before mirroring. */ +PureResult pure_list_read(PureList *, const unsigned char *bytes, size_t size); void pure_undo_rollback(PureUndo *); void pure_undo_commit(PureUndo *); void pure_undo_free(PureUndo *); diff --git a/prototypes/connections/module-save.c b/prototypes/connections/module-save.c new file mode 100644 index 0000000..40b3934 --- /dev/null +++ b/prototypes/connections/module-save.c @@ -0,0 +1,291 @@ +#include "module-save.h" +#include "module-text.h" +#include +#include +#include +#include + +/* Stored forms. values: "PVS1", then per state {u8 type, u8 name length, + * name, binary64 bits (little-endian)}. A list: "PLS1", u8 field count, per + * field {u8 0 number | 1 text, u8 name length, name}, then its items + * (module-list.h). A text state: its UTF-8 bytes. */ +static const unsigned char values_magic[4] = {'P', 'V', 'S', '1'}, list_magic[4] = {'P', 'L', 'S', '1'}; +static const PureVersion unsaved = {UINT64_MAX, UINT64_MAX}; + +bool pure_save_enabled(const PureApp *app) { return app->data.state && app->data.save; } + +static PureResult open_state(PureApp *app, const char *prefix, const char *name, PureDraft **out) { + char key[64]; + snprintf(key, sizeof(key), "%s%s", prefix, name); + return app->data.state(app->data.context, key, out); +} + +/* The whole value of a draft, contiguous; free it. */ +typedef struct { unsigned char *bytes; size_t used; } Whole; + +static bool gather(void *context, PureSpan span) { + Whole *w = context; + memcpy(w->bytes + w->used, span.data, span.size); + w->used += span.size; + return true; +} + +static PureResult read_whole(const PureDraft *draft, unsigned char **out, size_t *size) { + *size = pure_draft_size(draft); + Whole w = {malloc(*size ? *size : 1), 0}; + if (!w.bytes) return PURE_ALLOCATION; + PureResult result = pure_draft_read(draft, 0, *size, gather, &w); + if (result != PURE_OK) { free(w.bytes); return result; } + *out = w.bytes; + return PURE_OK; +} + +static bool name_ok(const unsigned char *name, size_t length) { + if (!length || length >= PURE_MODULE_NAME) return false; + for (size_t i = 0; i < length; ++i) { + unsigned char c = name[i]; + bool letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + if (!letter && !(i && c >= '0' && c <= '9')) return false; + } + return true; +} + +PureResult pure_save_open_text(PureApp *app, const PureModule *m, uint32_t state, PureDraft **out, + PureVersion *saved) { + PureResult result = open_state(app, "text-", m->state_names[state], out); + if (result != PURE_OK) return result; + *saved = pure_draft_version(*out); + if (!pure_draft_revision(*out)) { + uint32_t constant = m->state_ref[state]; + PureSpan initial = {m->constant_data + m->constant_offset[constant], m->constant_length[constant]}; + result = pure_draft_splice(*out, 0, pure_draft_size(*out), initial); + *saved = unsaved; + } else { + unsigned char *bytes; + size_t size; + result = read_whole(*out, &bytes, &size); + if (result == PURE_OK) { + if (!pure_utf8_valid((PureSpan){bytes, size})) result = PURE_STORE_ERROR; + free(bytes); + } + } + if (result != PURE_OK) { pure_draft_close(*out); *out = NULL; } + return result; +} + +static size_t list_header(const PureModule *m, uint32_t record, unsigned char *out) { + const PureRecord *r = &m->record[record]; + size_t at = 0; + memcpy(out, list_magic, 4); + at = 4; + out[at++] = (unsigned char)r->count; + for (unsigned f = 0; f < r->count; ++f) { + const char *name = m->record_field_names[r->first + f]; + out[at++] = m->record_field_text[r->first + f]; + out[at++] = (unsigned char)strlen(name); + memcpy(out + at, name, strlen(name)); + at += strlen(name); + } + return at; +} + +PureResult pure_save_rewrite_list(const PureModule *m, uint32_t state, PureList *list, PureDraft *draft) { + unsigned char header[5 + PURE_RECORD_FIELDS * (2 + PURE_MODULE_NAME)]; + size_t length = list_header(m, m->state_ref[state], header); + PureVersion before = pure_draft_version(draft); + pure_draft_hold(draft, true); + PureResult result = pure_draft_splice(draft, 0, pure_draft_size(draft), (PureSpan){header, length}); + pure_list_mirror(list, draft, length); + if (result == PURE_OK) result = pure_list_write(list); + if (result != PURE_OK) { + pure_draft_restore(draft, before); + pure_list_mirror(list, NULL, 0); + } + pure_draft_hold(draft, false); + return result; +} + +PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint32_t state, PureList **out, PureDraft **draft, + PureVersion *saved) { + uint32_t record = m->state_ref[state]; + const PureRecord *r = &m->record[record]; + *out = NULL; + PureResult result = open_state(app, "list-", m->state_names[state], draft); + if (result != PURE_OK) return result; + unsigned char *bytes = NULL; + size_t size = 0; + if (pure_draft_revision(*draft)) result = read_whole(*draft, &bytes, &size); + /* The stored record: its fields' kinds and names. */ + char names[PURE_RECORD_FIELDS][PURE_MODULE_NAME] = {{0}}; + uint8_t text[PURE_RECORD_FIELDS] = {0}; + unsigned count = 0; + size_t at = 0; + bool stored = result == PURE_OK && bytes; + if (stored) { + bool ok = size >= 5 && !memcmp(bytes, list_magic, 4) && bytes[4] <= PURE_RECORD_FIELDS; + count = ok ? bytes[4] : 0; + at = 5; + for (unsigned f = 0; ok && f < count; ++f) { + ok = size - at >= 2 && bytes[at] <= 1 && size - at - 2 >= bytes[at + 1] && name_ok(bytes + at + 2, bytes[at + 1]); + if (!ok) break; + text[f] = bytes[at]; + memcpy(names[f], bytes + at + 2, bytes[at + 1]); + for (unsigned k = 0; k < f; ++k) ok = ok && strcmp(names[k], names[f]); + at += 2 + bytes[at + 1]; + } + if (!ok) result = PURE_STORE_ERROR; + } + PureList *from = NULL; + if (result == PURE_OK && stored) { + from = pure_list_new(count, text); + result = from ? pure_list_read(from, bytes + at, size - at) : PURE_ALLOCATION; + } + free(bytes); + bool same = stored && count == r->count; + for (unsigned f = 0; same && f < count; ++f) + same = text[f] == m->record_field_text[r->first + f] && !strcmp(names[f], m->record_field_names[r->first + f]); + if (result == PURE_OK && same) { + *out = from; + pure_list_mirror(from, *draft, at); + *saved = pure_draft_version(*draft); + return PURE_OK; + } + if (result == PURE_OK) { + /* New, or stored with another record: its items move by field name. */ + const char *field_names[PURE_RECORD_FIELDS]; + for (unsigned f = 0; f < count; ++f) field_names[f] = names[f]; + if (from) result = pure_list_migrate(out, from, field_names, text, count, m, record); + else if (!(*out = pure_list_new(r->count, m->record_field_text + r->first))) result = PURE_ALLOCATION; + } + pure_list_free(from); + if (result == PURE_OK) result = pure_save_rewrite_list(m, state, *out, *draft); + if (result != PURE_OK) { + pure_list_free(*out); + pure_draft_close(*draft); + *out = NULL; + *draft = NULL; + return result; + } + *saved = unsaved; + return PURE_OK; +} + +static bool scalar(uint8_t type) { + return type == PURE_STATE_NUM || type == PURE_STATE_CURSOR || type == PURE_STATE_INDEX; +} + +/* A stored value still fits its state: a cursor on a character boundary of + * its text, an index on an item of its list (or 0), a finite number. */ +static bool fits(const PureApp *app, const PureModule *m, uint32_t state, double value) { + if (!isfinite(value)) return false; + if (m->state_type[state] == PURE_STATE_NUM) return true; + if (value < 0 || value != floor(value)) return false; + if (m->state_type[state] == PURE_STATE_INDEX) { + size_t count = pure_list_count(app->lists[m->state_ref[state]]); + return value <= (count ? (double)(count - 1) : 0); + } + const PureDraft *text = app->texts[m->state_ref[state]]; + size_t size = pure_draft_size(text), at = (size_t)value; + if (value > (double)size) return false; + if (at == size) return true; + unsigned char byte = 0; + Whole w = {&byte, 0}; + return pure_draft_read(text, at, 1, gather, &w) == PURE_OK && (byte & 0xC0) != 0x80; +} + +static size_t encode_values(const PureModule *m, const double *values, unsigned char *out) { + size_t at = 4; + memcpy(out, values_magic, 4); + for (uint32_t i = 0; i < m->states; ++i) { + if (!scalar(m->state_type[i])) continue; + size_t length = strlen(m->state_names[i]); + uint64_t bits; + memcpy(&bits, &values[i], sizeof(bits)); + out[at++] = m->state_type[i]; + out[at++] = (unsigned char)length; + memcpy(out + at, m->state_names[i], length); + at += length; + for (unsigned b = 0; b < 8; ++b) out[at++] = (unsigned char)(bits >> (8 * b)); + } + return at; +} + +enum { VALUES_BYTES = 4 + PURE_MODULE_STATES * (2 + PURE_MODULE_NAME + 8) }; + +PureResult pure_save_open_values(PureApp *app, const PureModule *m, double *values, bool load) { + bool any = false; + for (uint32_t i = 0; i < m->states; ++i) any = any || scalar(m->state_type[i]); + if (!any || app->stored_values) return PURE_OK; + PureResult result = open_state(app, "", "values", &app->stored_values); + if (result != PURE_OK || !load || !pure_draft_revision(app->stored_values)) return result; + unsigned char *bytes; + size_t size; + if ((result = read_whole(app->stored_values, &bytes, &size)) != PURE_OK) return result; + bool ok = size >= 4 && !memcmp(bytes, values_magic, 4); + for (size_t at = 4; ok && at < size;) { + ok = size - at >= 2 && size - at - 2 >= (size_t)bytes[at + 1] + 8 && name_ok(bytes + at + 2, bytes[at + 1]); + if (!ok) break; + char name[PURE_MODULE_NAME] = {0}; + memcpy(name, bytes + at + 2, bytes[at + 1]); + uint8_t type = bytes[at]; + at += 2 + bytes[at + 1]; + uint64_t bits = 0; + for (unsigned b = 0; b < 8; ++b) bits |= (uint64_t)bytes[at + b] << (8 * b); + at += 8; + double value; + memcpy(&value, &bits, sizeof(value)); + int state = pure_module_state(m, name); + if (state >= 0 && m->state_type[state] == type && scalar(type) && fits(app, m, (uint32_t)state, value)) + values[state] = value; + } + free(bytes); + return ok ? PURE_OK : PURE_STORE_ERROR; +} + +static bool same_version(PureVersion a, PureVersion b) { return a.ref == b.ref && a.bytes == b.bytes; } + +PureResult pure_save_commit(PureApp *app, const double *values, void *document, int64_t *revision) { + if (!pure_save_enabled(app)) + return document ? app->data.commit(app->data.context, document, revision) : PURE_OK; + const PureModule *m = app->module; + PureDraft *drafts[2 * PURE_MODULE_STATES + 1]; + uint32_t states[2 * PURE_MODULE_STATES + 1]; + size_t count = 0; + for (uint32_t i = 0; i < m->states; ++i) { + PureDraft *draft = m->state_type[i] == PURE_STATE_TEXT ? app->texts[i] : + m->state_type[i] == PURE_STATE_LIST ? app->list_drafts[i] : NULL; + if (draft && !same_version(pure_draft_version(draft), app->saved[i])) { + states[count] = i; + drafts[count++] = draft; + } + } + /* Numbers, cursors and indices: stored again only when their bytes change. */ + PureVersion values_before = {0, 0}; + bool values_changed = false; + if (app->stored_values) { + unsigned char encoded[VALUES_BYTES], *stored; + size_t length = encode_values(m, values, encoded), size; + PureResult result = read_whole(app->stored_values, &stored, &size); + if (result != PURE_OK) return result; + values_changed = size != length || memcmp(stored, encoded, length); + free(stored); + if (values_changed) { + values_before = pure_draft_version(app->stored_values); + pure_draft_hold(app->stored_values, true); + result = pure_draft_splice(app->stored_values, 0, size, (PureSpan){encoded, length}); + if (result != PURE_OK) { pure_draft_hold(app->stored_values, false); return result; } + states[count] = UINT32_MAX; + drafts[count++] = app->stored_values; + } + } + if (!count && !document) return PURE_OK; + int64_t ignored; + PureResult result = app->data.save(app->data.context, document, drafts, count, document ? revision : &ignored); + for (size_t k = 0; result == PURE_OK && k < count; ++k) + if (states[k] != UINT32_MAX) app->saved[states[k]] = pure_draft_version(drafts[k]); + if (values_changed) { + if (result != PURE_OK) pure_draft_restore(app->stored_values, values_before); + pure_draft_hold(app->stored_values, false); + } + return result; +} diff --git a/prototypes/connections/module-save.h b/prototypes/connections/module-save.h new file mode 100644 index 0000000..90e0701 --- /dev/null +++ b/prototypes/connections/module-save.h @@ -0,0 +1,30 @@ +#ifndef PURE_MODULE_SAVE_H +#define PURE_MODULE_SAVE_H +#include "module.h" + +/* App states a data host keeps (docs/design/008-module-format.md), internal + * to the module executor. Each text state is a stored draft, each list state + * a stored draft its list mirrors, and numbers, cursors and indices are one + * stored value by name. A save publishes what changed, all or nothing. */ + +bool pure_save_enabled(const PureApp *); +/* A text state of m: its stored value, or its initial constant when none. */ +PureResult pure_save_open_text(PureApp *, const PureModule *m, uint32_t state, PureDraft **out, PureVersion *saved); +/* A list state of m: its stored items, fields moved by name when the stored + * record differs from m's. */ +PureResult pure_save_open_list(PureApp *, const PureModule *m, uint32_t state, PureList **out, PureDraft **draft, + PureVersion *saved); +/* Open the stored numbers, cursors and indices of m and, with load, put each + * stored one of the same name and kind that still fits into values. */ +PureResult pure_save_open_values(PureApp *, const PureModule *m, double *values, bool load); +/* Store a list whose record changed in its draft, header and items anew. */ +PureResult pure_save_rewrite_list(const PureModule *m, uint32_t state, PureList *list, PureDraft *draft); +/* Publish every state changed since the last save (values are the pending + * numbers), with document when not NULL; revision is the document's. */ +PureResult pure_save_commit(PureApp *, const double *values, void *document, int64_t *revision); + +/* A list with record's fields, taking from `from` each field of the same name + * and kind and giving the others their defaults (module.c). */ +PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names, + const uint8_t *text, unsigned count, const PureModule *m, uint32_t record); +#endif diff --git a/prototypes/connections/module-store.c b/prototypes/connections/module-store.c index 941957b..6335903 100644 --- a/prototypes/connections/module-store.c +++ b/prototypes/connections/module-store.c @@ -38,7 +38,26 @@ static void close_document(void *context, void *document) { pure_draft_close(document); } +static PureResult state(void *context, const char *name, PureDraft **out) { + return pure_state_open(context, name, out); +} + +/* The document and the app's changed states in one tree commit. */ +static PureResult save(void *context, void *document, PureDraft *const *states, size_t count, int64_t *revision) { + (void)context; + PureDraft *drafts[2 * PURE_MODULE_STATES + 2] = {0}; + int64_t revisions[2 * PURE_MODULE_STATES + 2] = {0}; + size_t n = 0; + if (count > 2 * PURE_MODULE_STATES + 1) return PURE_LIMIT; + if (document) drafts[n++] = document; + for (size_t i = 0; i < count; ++i) drafts[n++] = states[i]; + PureResult result = pure_draft_commit_all(drafts, n, revisions); + if (document) *revision = revisions[0]; + return result; +} + PureData pure_tree_data(const char *path) { return (PureData){.context = (void *)path, .open = open_document, .version = version, - .read = read, .splice = splice, .commit = commit, .close = close_document}; + .read = read, .splice = splice, .commit = commit, .close = close_document, + .state = state, .save = save}; } diff --git a/prototypes/connections/module-store.h b/prototypes/connections/module-store.h index 5f1c1a8..0a4b494 100644 --- a/prototypes/connections/module-store.h +++ b/prototypes/connections/module-store.h @@ -1,7 +1,8 @@ #ifndef PURE_MODULE_STORE_H #define PURE_MODULE_STORE_H #include "module.h" -/* Keyed module collections on the selected tree store: one event opens the - * message key as a held draft and commits it. The path outlives the app. */ +/* Keyed module collections and the app's states on the selected tree store: + * one event opens the message key as a held draft and commits it together + * with the states the event changed. The path outlives the app. */ PureData pure_tree_data(const char *path); #endif diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c index 3d3ee3e..362919b 100644 --- a/prototypes/connections/module.c +++ b/prototypes/connections/module.c @@ -1,4 +1,5 @@ #include "module.h" +#include "module-save.h" #include "module-text.h" #include #include @@ -1056,14 +1057,27 @@ static PureList *new_list(const PureModule *m, uint32_t record) { PureResult pure_app_start(PureApp *app, const PureModule *module, PureData data) { *app = (PureApp){.module = module, .data = data, .fault_pc = UINT32_MAX}; memcpy(app->values, module->initial, sizeof(double) * module->states); - for (uint32_t i = 0; i < module->states; ++i) { - PureResult result = PURE_OK; - if (module->state_type[i] == PURE_STATE_TEXT) result = new_text(&app->texts[i], module, module->state_ref[i]); - if (module->state_type[i] == PURE_STATE_LIST && !(app->lists[i] = new_list(module, module->state_ref[i]))) - result = PURE_ALLOCATION; - if (result != PURE_OK) { pure_app_stop(app); return result; } + bool kept = pure_save_enabled(app); + PureResult result = PURE_OK; + for (uint32_t i = 0; i < module->states && result == PURE_OK; ++i) { + if (module->state_type[i] == PURE_STATE_TEXT) + result = kept ? pure_save_open_text(app, module, i, &app->texts[i], &app->saved[i]) : + new_text(&app->texts[i], module, module->state_ref[i]); + if (module->state_type[i] == PURE_STATE_LIST) { + if (kept) result = pure_save_open_list(app, module, i, &app->lists[i], &app->list_drafts[i], &app->saved[i]); + else if (!(app->lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION; + } } - return PURE_OK; + /* Stored numbers, cursors and indices apply once their texts and lists are in. */ + if (result == PURE_OK && kept) result = pure_save_open_values(app, module, app->values, true); + if (result == PURE_OK) result = pure_app_save(app); + if (result != PURE_OK) pure_app_stop(app); + return result; +} + +PureResult pure_app_save(PureApp *app) { + int64_t revision; + return pure_save_commit(app, app->values, NULL, &revision); } void pure_app_finish(PureApp *app) { @@ -1081,33 +1095,45 @@ void pure_app_stop(PureApp *app) { for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) { pure_draft_close(app->texts[i]); pure_list_free(app->lists[i]); + pure_draft_close(app->list_drafts[i]); } + pure_draft_close(app->stored_values); pure_undo_free(&app->undo); free(app->arena.bytes); free(app->view_arena.bytes); memset(app, 0, sizeof(*app)); } -/* Every text draft is held for the event; on failure each returns to the - * version it had when the event began. */ -typedef struct { PureVersion before[PURE_MODULE_STATES]; } Texts; +/* Every text draft and stored list draft is held for the event; on failure + * each returns to the version it had when the event began. */ +typedef struct { PureVersion before[PURE_MODULE_STATES], lists[PURE_MODULE_STATES]; } Texts; static void hold_texts(PureApp *app, Texts *texts) { - for (uint32_t i = 0; i < app->module->states; ++i) + for (uint32_t i = 0; i < app->module->states; ++i) { if (app->texts[i]) { pure_draft_hold(app->texts[i], true); texts->before[i] = pure_draft_version(app->texts[i]); } + if (app->list_drafts[i]) { + pure_draft_hold(app->list_drafts[i], true); + texts->lists[i] = pure_draft_version(app->list_drafts[i]); + } + } } -/* Texts return to their versions and lists undo their log, or both keep - * what the event did. */ +/* Texts and stored lists return to their versions and lists undo their log, + * or all keep what the event did. */ static void settle(PureApp *app, const Texts *texts, bool keep) { - for (uint32_t i = 0; i < app->module->states; ++i) + for (uint32_t i = 0; i < app->module->states; ++i) { if (app->texts[i]) { if (!keep) pure_draft_restore(app->texts[i], texts->before[i]); pure_draft_hold(app->texts[i], false); } + if (app->list_drafts[i]) { + if (!keep) pure_draft_restore(app->list_drafts[i], texts->lists[i]); + pure_draft_hold(app->list_drafts[i], false); + } + } if (keep) pure_undo_commit(&app->undo); else pure_undo_rollback(&app->undo); } @@ -1126,11 +1152,15 @@ static PureResult run_handlers(PureApp *app, uint32_t event, Event *ev, double * return PURE_OK; } +/* A key or text event stores what it changed before it counts; a frame + * leaves that to the next event (frames come 60 times a second). */ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { double pending[PURE_MODULE_STATES]; Texts texts; hold_texts(app, &texts); PureResult result = run_handlers(app, event, ev, pending); + int64_t revision; + if (result == PURE_OK && event) result = pure_save_commit(app, pending, NULL, &revision); settle(app, &texts, result == PURE_OK); if (result == PURE_OK) { memcpy(app->values, pending, sizeof(pending)); @@ -1208,8 +1238,8 @@ PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64 if (result == PURE_OK && app->has_reply && ((app->reply.source == FROM_DOCUMENT && ev.modified) || app->reply.source == FROM_TEXT)) result = keep_reply(app); - if (result == PURE_OK && ev.modified) - result = app->data.commit(app->data.context, app->document, &app->revision); + if (result == PURE_OK) + result = pure_save_commit(app, pending, ev.modified ? app->document : NULL, &app->revision); /* States, texts and lists change only with a successful commit (or no document edit). */ settle(app, &texts, result == PURE_OK); if (result == PURE_OK) { @@ -1278,21 +1308,18 @@ PureResult pure_app_cell_text(PureApp *app, uint32_t column, size_t row, bool (* return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result; } -/* A kept list keeps its items. When its record changed, each field takes the - * old field of the same name and kind, or its default. */ -static PureResult keep_list(PureList **out, PureList *from, const PureModule *old, uint32_t old_record, - const PureModule *m, uint32_t record) { - const PureRecord *r = &m->record[record], *o = &old->record[old_record]; +/* A list with record's fields: each takes the field of `from` with the same + * name and kind, or its default. */ +PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names, + const uint8_t *text, unsigned count, const PureModule *m, uint32_t record) { + const PureRecord *r = &m->record[record]; int source[PURE_RECORD_FIELDS]; - bool same = r->count == o->count; for (unsigned f = 0; f < r->count; ++f) { source[f] = -1; - for (unsigned k = 0; k < o->count; ++k) - if (!strcmp(m->record_field_names[r->first + f], old->record_field_names[o->first + k]) && - m->record_field_text[r->first + f] == old->record_field_text[o->first + k]) source[f] = (int)k; - same = same && source[f] == (int)f; + for (unsigned k = 0; k < count; ++k) + if (!strcmp(m->record_field_names[r->first + f], names[k]) && m->record_field_text[r->first + f] == text[k]) + source[f] = (int)k; } - if (same) { *out = from; return PURE_OK; } if (!(*out = new_list(m, record))) return PURE_ALLOCATION; PureResult result = PURE_OK; for (size_t at = 0; result == PURE_OK && at < pure_list_count(from); ++at) { @@ -1311,26 +1338,55 @@ static PureResult keep_list(PureList **out, PureList *from, const PureModule *ol return result; } +/* A kept list keeps its items; with the same record it is the same list. */ +static PureResult keep_list(PureList **out, PureList *from, const PureModule *old, uint32_t old_record, + const PureModule *m, uint32_t record) { + const PureRecord *r = &m->record[record], *o = &old->record[old_record]; + const char *names[PURE_RECORD_FIELDS]; + bool same = r->count == o->count; + for (unsigned k = 0; k < o->count; ++k) { + names[k] = old->record_field_names[o->first + k]; + same = same && !strcmp(names[k], m->record_field_names[r->first + k]) && + old->record_field_text[o->first + k] == m->record_field_text[r->first + k]; + } + if (same) { *out = from; return PURE_OK; } + return pure_list_migrate(out, from, names, old->record_field_text + o->first, o->count, m, record); +} + +/* States keep their values by name and kind. With stored states, a state new + * to this app starts from its stored value, a list whose record changed is + * stored again, and the next event or save publishes the new module's states; + * a state the new module drops stays stored as it was. */ PureResult pure_app_replace(PureApp *app, const PureModule *module) { pure_app_finish(app); const PureModule *old = app->module; + bool stored = pure_save_enabled(app); double values[PURE_MODULE_STATES] = {0}; - PureDraft *texts[PURE_MODULE_STATES] = {0}; + PureDraft *texts[PURE_MODULE_STATES] = {0}, *list_drafts[PURE_MODULE_STATES] = {0}; PureList *lists[PURE_MODULE_STATES] = {0}; + PureVersion saved[PURE_MODULE_STATES] = {{0, 0}}, before[PURE_MODULE_STATES] = {{0, 0}}; int kept[PURE_MODULE_STATES]; + bool rewrite[PURE_MODULE_STATES] = {false}; PureResult result = PURE_OK; for (uint32_t i = 0; i < module->states; ++i) { int previous = pure_module_state(old, module->state_names[i]); kept[i] = previous >= 0 && old->state_type[previous] == module->state_type[i] ? previous : -1; + if (kept[i] >= 0) saved[i] = app->saved[kept[i]]; if (module->state_type[i] == PURE_STATE_TEXT) { if (kept[i] >= 0) texts[i] = app->texts[kept[i]]; - else if (result == PURE_OK) result = new_text(&texts[i], module, module->state_ref[i]); + else if (result == PURE_OK) + result = stored ? pure_save_open_text(app, module, i, &texts[i], &saved[i]) : + new_text(&texts[i], module, module->state_ref[i]); } else if (module->state_type[i] == PURE_STATE_LIST) { if (result != PURE_OK) continue; - if (kept[i] >= 0) + if (kept[i] >= 0) { result = keep_list(&lists[i], app->lists[kept[i]], old, old->state_ref[kept[i]], module, module->state_ref[i]); - else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION; + list_drafts[i] = app->list_drafts[kept[i]]; + rewrite[i] = result == PURE_OK && list_drafts[i] && lists[i] != app->lists[kept[i]]; + } else if (stored) { + result = pure_save_open_list(app, module, i, &lists[i], &list_drafts[i], &saved[i]); + } else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION; } else values[i] = kept[i] >= 0 ? app->values[kept[i]] : module->initial[i]; } /* A cursor or index keeps its position only while its text or list is the @@ -1341,25 +1397,50 @@ PureResult pure_app_replace(PureApp *app, const PureModule *module) { bool same = kept[i] >= 0 && kept[owner] >= 0 && old->state_ref[kept[i]] == (unsigned)kept[owner]; values[i] = same ? app->values[kept[i]] : 0; } + if (result == PURE_OK && stored) result = pure_save_open_values(app, module, values, false); + /* Store the lists whose record changed last, so that a failure can put + * every draft back as it was. */ + for (uint32_t i = 0; i < module->states; ++i) + if (rewrite[i]) { + before[i] = pure_draft_version(list_drafts[i]); + pure_draft_hold(list_drafts[i], true); + } + for (uint32_t i = 0; i < module->states && result == PURE_OK; ++i) + if (rewrite[i]) result = pure_save_rewrite_list(module, i, lists[i], list_drafts[i]); + for (uint32_t i = 0; i < module->states; ++i) + if (rewrite[i]) { + if (result != PURE_OK) pure_draft_restore(list_drafts[i], before[i]); + pure_draft_hold(list_drafts[i], false); + } if (result != PURE_OK) { for (uint32_t i = 0; i < module->states; ++i) { - if (texts[i] && kept[i] < 0) pure_draft_close(texts[i]); - if (lists[i] && (kept[i] < 0 || lists[i] != app->lists[kept[i]])) pure_list_free(lists[i]); + if (kept[i] >= 0) { + if (lists[i] && lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]); + continue; + } + pure_draft_close(texts[i]); + pure_list_free(lists[i]); + pure_draft_close(list_drafts[i]); } return result; } for (uint32_t i = 0; i < old->states; ++i) { - bool reused = false; - for (uint32_t k = 0; k < module->states; ++k) + bool reused = false, draft_reused = false; + for (uint32_t k = 0; k < module->states; ++k) { reused = reused || (texts[k] && texts[k] == app->texts[i]) || (lists[k] && lists[k] == app->lists[i]); + draft_reused = draft_reused || (list_drafts[k] && list_drafts[k] == app->list_drafts[i]); + } if (!reused) { pure_draft_close(app->texts[i]); pure_list_free(app->lists[i]); } + if (!draft_reused) pure_draft_close(app->list_drafts[i]); } memcpy(app->values, values, sizeof(values)); memcpy(app->texts, texts, sizeof(texts)); memcpy(app->lists, lists, sizeof(lists)); + memcpy(app->list_drafts, list_drafts, sizeof(list_drafts)); + memcpy(app->saved, saved, sizeof(saved)); app->module = module; return PURE_OK; } diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h index dd20d4a..d2d4b7a 100644 --- a/prototypes/connections/module.h +++ b/prototypes/connections/module.h @@ -121,6 +121,12 @@ typedef struct { PureResult (*splice)(void *context, void *document, size_t offset, size_t remove, PureSpan insert); PureResult (*commit)(void *context, void *document, int64_t *revision); void (*close)(void *context, void *document); + /* Optional: the host keeps the app's states. state opens a runtime-owned + * value by name as a draft (a missing one is empty at revision 0); save + * publishes the given state drafts together with document (or NULL), + * all or nothing, and names the document's new revision. */ + PureResult (*state)(void *context, const char *name, PureDraft **out); + PureResult (*save)(void *context, void *document, PureDraft *const *states, size_t count, int64_t *revision); } PureData; /* A byte or text value inside one event or view: a range of the payload, a @@ -131,7 +137,11 @@ typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena; /* One app instance: its states and at most one open event. Text states are * runtime-owned drafts; cursors are positions the runtime keeps on character * boundaries; lists are runtime-owned and an index always names an item - * (or 0 in an empty list). Every event is atomic: on failure nothing changes. */ + * (or 0 in an empty list). Every event is atomic: on failure nothing changes. + * When the data host keeps states, the app starts from the stored ones and + * every key, text and message event stores what it changed with its own + * commit (with the message's document); frame events are stored with the + * next one, or by pure_app_save. */ typedef struct { const PureModule *module; PureData data; @@ -139,6 +149,9 @@ typedef struct { PureDraft *texts[PURE_MODULE_STATES]; PureList *lists[PURE_MODULE_STATES]; PureUndo undo; /* list changes of the current event */ + PureDraft *list_drafts[PURE_MODULE_STATES]; /* stored lists, when kept */ + PureDraft *stored_values; /* numbers, cursors and indices, when kept */ + PureVersion saved[PURE_MODULE_STATES]; /* text and list versions at the last save */ PureFault fault; uint32_t fault_pc; uint64_t ticks, events; @@ -153,6 +166,9 @@ typedef struct { } PureApp; PureResult pure_app_start(PureApp *, const PureModule *, PureData data); +/* Store every state changed since the last save (after frame events). */ +PureResult pure_app_save(PureApp *); +/* Leaves unsaved frame changes unsaved: call pure_app_save first to keep them. */ void pure_app_stop(PureApp *); PureResult pure_app_frame(PureApp *); PureResult pure_app_input(PureApp *, uint32_t input, PureEdge); diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c index 8a9788f..7b6e253 100644 --- a/prototypes/connections/pure-run.c +++ b/prototypes/connections/pure-run.c @@ -1,12 +1,15 @@ #define _POSIX_C_SOURCE 200809L -#include "module.h" +#include "module-store.h" +#include "store.h" #include #include #include /* Test host for a verified module: reads commands from stdin and prints one - * JSON snapshot per command. Not a product interface. - * down NAME | up NAME | frame [N] | type NAME HEX|- | replace PATH | show + * JSON snapshot per command. Not a product interface. With --store the app's + * states live in that tree store: it starts from them, and frame changes are + * saved at the end of input or by save. + * down NAME | up NAME | frame [N] | type NAME HEX|- | replace PATH | save | show * Text states and text views print as JSON strings; cursors as byte positions; * lists as arrays of objects; indices as positions; a table view as an array * with an object of its columns per row. A failed view or cell is null. */ @@ -131,13 +134,16 @@ static int hex(char c) { int main(int argc, char **argv) { static PureModule modules[2]; - if (argc != 2 || !load(argv[1], &modules[0])) { - if (argc != 2) fputs("pure-run MODULE < commands\n", stderr); + bool stored = argc == 4 && !strcmp(argv[2], "--store"); + if ((argc != 2 && !stored) || !load(argv[1], &modules[0])) { + if (argc != 2 && !stored) fputs("pure-run MODULE [--store PATH] < commands\n", stderr); return 2; } + if (stored && pure_store_init(argv[3]) != PURE_OK) { fprintf(stderr, "cannot open store %s\n", argv[3]); return 2; } unsigned active = 0; PureApp app; - if (pure_app_start(&app, &modules[active], (PureData){0}) != PURE_OK) { fputs("cannot start\n", stderr); return 2; } + PureResult started = pure_app_start(&app, &modules[active], stored ? pure_tree_data(argv[3]) : (PureData){0}); + if (started != PURE_OK) { fprintf(stderr, "cannot start (%d)\n", (int)started); return 2; } static char line[1 << 16]; static unsigned char text[1 << 15]; int status = 0; @@ -167,10 +173,13 @@ int main(int argc, char **argv) { if (!load(argument, &modules[!active])) failure = "invalid module"; else if (pure_app_replace(&app, &modules[!active]) != PURE_OK) failure = "replace failed"; else active = !active; + } else if (!strcmp(command, "save")) { + if (pure_app_save(&app) != PURE_OK) failure = "save failed"; } else if (strcmp(command, "show")) failure = "unknown command"; if (failure) status = 1; show(&app, failure); } + if (pure_app_save(&app) != PURE_OK) { fputs("cannot save\n", stderr); status = 1; } pure_app_stop(&app); return status; } diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c index a4e746c..64409b2 100644 --- a/prototypes/connections/test-module.c +++ b/prototypes/connections/test-module.c @@ -1,12 +1,27 @@ +#define _POSIX_C_SOURCE 200809L #include "module.h" +#include "module-store.h" #include "module-text.h" +#include "store.h" +#include #include #include #include #include +#include +#include #define CHECK(x) do { if (!(x)) { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); abort(); } } while (0) +/* Store faults: a stage fails (or the process dies there when crash is set). */ +static int fail_stage = -1; +static int crash; +int pure_store_test_fault(PureStoreStage stage) { + if ((int)stage != fail_stage) return 0; + if (crash) _exit(77); + return 1; +} + typedef struct { const char *name; double initial; } S; typedef struct { const char *name; uint32_t type; } F; /* kind 0 is a key input; otherwise PURE_KIND_MESSAGE with fields. */ @@ -477,7 +492,7 @@ static void fake_close(void *context, void *document) { } static PureData fake_data(Memory *m) { - return (PureData){m, fake_open, fake_version, fake_read, fake_splice, fake_commit, fake_close}; + return (PureData){m, fake_open, fake_version, fake_read, fake_splice, fake_commit, fake_close, NULL, NULL}; } typedef struct { unsigned char bytes[256]; size_t size, spans; } Collected; @@ -1262,6 +1277,303 @@ static void table_mutations(void) { printf("{\"table_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); } +/* ---- Stored states ---------------------------------------------------- */ + +static char *new_store(void) { + static char path[64]; + snprintf(path, sizeof(path), "/tmp/pure-module-store-XXXXXX"); + CHECK(mkdtemp(path) && pure_store_init(path) == PURE_OK); + return path; +} + +static void remove_store(const char *path) { + DIR *entries = opendir(path); + CHECK(entries); + struct dirent *entry; + while ((entry = readdir(entries))) + if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) CHECK(unlinkat(dirfd(entries), entry->d_name, 0) == 0); + CHECK(closedir(entries) == 0 && rmdir(path) == 0); +} + +typedef struct { unsigned char bytes[4096]; size_t size; } Bytes; + +static bool bytes_span(void *context, PureSpan span) { + Bytes *b = context; + CHECK(b->size + span.size <= sizeof(b->bytes)); + memcpy(b->bytes + b->size, span.data, span.size); + b->size += span.size; + return true; +} + +/* Two apps of one module hold equal states: numbers bit for bit, texts and + * list items byte for byte. */ +static void same_states(const PureApp *a, const PureApp *b) { + const PureModule *m = a->module; + for (uint32_t i = 0; i < m->states; ++i) { + if (m->state_type[i] == PURE_STATE_TEXT) { + static Bytes x, y; + x.size = y.size = 0; + CHECK(pure_draft_read(a->texts[i], 0, pure_draft_size(a->texts[i]), bytes_span, &x) == PURE_OK); + CHECK(pure_draft_read(b->texts[i], 0, pure_draft_size(b->texts[i]), bytes_span, &y) == PURE_OK); + CHECK(x.size == y.size && !memcmp(x.bytes, y.bytes, x.size)); + } else if (m->state_type[i] == PURE_STATE_LIST) { + const PureRecord *r = &m->record[m->state_ref[i]]; + CHECK(pure_list_count(a->lists[i]) == pure_list_count(b->lists[i])); + for (size_t at = 0; at < pure_list_count(a->lists[i]); ++at) + for (unsigned f = 0; f < r->count; ++f) { + PureCell x = pure_list_get(a->lists[i], at, f), y = pure_list_get(b->lists[i], at, f); + CHECK(!memcmp(&x.num, &y.num, sizeof(x.num)) && x.size == y.size && (!x.size || !memcmp(x.text, y.text, x.size))); + } + } else CHECK(!memcmp(&a->values[i], &b->values[i], sizeof(double))); + } +} + +/* The same random events on an app in memory and on one whose states are + * stored and which restarts from them every few events: equal after every + * step, failed events included. */ +static void stored_equivalence(Spec spec, const char *const *words, size_t word_count, uint32_t seed) { + char *path = new_store(); + CHECK(load(spec, &module_a) == PURE_OK); + PureApp memory, stored; + CHECK(pure_app_start(&memory, &module_a, (PureData){0}) == PURE_OK); + CHECK(pure_app_start(&stored, &module_a, pure_tree_data(path)) == PURE_OK); + unsigned restarts = 0; + for (unsigned step = 0; step < 400; ++step) { + uint32_t input = next_random(&seed) % module_a.inputs; + PureResult a, b; + if (module_a.input[input].kind == PURE_KIND_TEXT) { + const char *word = words[next_random(&seed) % word_count]; + PureSpan text = {(const unsigned char *)word, strlen(word)}; + a = pure_app_text(&memory, input, text); + b = pure_app_text(&stored, input, text); + } else { + a = pure_app_input(&memory, input, PURE_EDGE_DOWN); + b = pure_app_input(&stored, input, PURE_EDGE_DOWN); + } + CHECK(a == b); + same_states(&memory, &stored); + if (next_random(&seed) % 7 == 0) { + pure_app_stop(&stored); + CHECK(pure_app_start(&stored, &module_a, pure_tree_data(path)) == PURE_OK); + same_states(&memory, &stored); + ++restarts; + } + } + CHECK(restarts > 20); + pure_app_stop(&memory); + pure_app_stop(&stored); + remove_store(path); +} + +/* One event changes a number, a text and a list; the process dies at each + * commit stage. Starting again finds all three as before or all as after. */ +enum { NOTE = 1, TASKS = 3 }; +static const S crash_nums[] = {{"n", 0}}; +static const T crash_states[] = {{"note", 1, 0}, {"at", 2, 1}, {"tasks", 3, 0}, {"i", 4, 3}}; +static const RF crash_fields[] = {{"title", 1, 0}, {"done", 0, 0}}; +static const R crash_records[] = {{"task", crash_fields, 2}}; +static const char *const crash_constants[] = {"", "x"}; +static const I crash_inputs[] = {KEY("go", "KeyG")}; +static const O crash_code[] = { + {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* n = n + 1 */ + {CUL, 2, 0}, {TK, 1, 0}, {TINS, 1, 0}, /* note.insert(at, "x") */ + {TK, 1, 0}, {L, 0, 0}, {LA, 3, 0}, /* tasks.append(task { "x", n }) */ + {XE, 3, 0}, {XS, 4, 0}, /* i = tasks.last */ +}; +static const H crash_handlers[] = {{1, 0, 12}}; + +static unsigned consistent(const PureApp *app) { + unsigned n = (unsigned)app->values[0]; + CHECK(app->values[0] == n && pure_draft_size(app->texts[NOTE]) == n && app->values[2] == n); + CHECK(pure_list_count(app->lists[TASKS]) == n && app->values[4] == (n ? n - 1 : 0)); + for (unsigned k = 0; k < n; ++k) CHECK(pure_list_get(app->lists[TASKS], k, 1).num == k + 1); + return n; +} + +static void stored_crashes(void) { + char *path = new_store(); + Spec spec = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0, + crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0}; + CHECK(load(spec, &module_a) == PURE_OK); + PureApp app; + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && consistent(&app) == 1); + pure_app_stop(&app); + unsigned expected = 1; + for (int stage = PURE_AFTER_HEADER; stage <= PURE_BEFORE_DIR_SYNC; ++stage) { + pid_t child = fork(); + CHECK(child >= 0); + if (!child) { + PureApp doomed; + if (pure_app_start(&doomed, &module_a, pure_tree_data(path)) != PURE_OK) _exit(1); + crash = 1; + fail_stage = stage; + pure_app_input(&doomed, 0, PURE_EDGE_DOWN); + _exit(0); + } + int status; + CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77); + if (stage == PURE_BEFORE_DIR_SYNC) ++expected; /* the head was written */ + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == expected); + pure_app_stop(&app); + } + /* A failed commit leaves the app and the store as they were; after an + * uncertain one the app no longer matches the store and its events fail + * until it starts again, from what the store has. */ + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + for (fail_stage = PURE_AFTER_HEADER; fail_stage <= PURE_BEFORE_RENAME; ++fail_stage) { + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_STORE_ERROR && consistent(&app) == expected); + } + fail_stage = PURE_BEFORE_DIR_SYNC; + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_UNCERTAIN && consistent(&app) == expected); + fail_stage = -1; + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_CONFLICT && consistent(&app) == expected); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == expected + 1); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && consistent(&app) == expected + 2); + pure_app_stop(&app); + remove_store(path); +} + +/* Frame changes are stored by the next event or by a save; a stop alone + * leaves them out. */ +static void stored_frames(void) { + char *path = new_store(); + static const O code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}}; + static const H handlers[] = {{0, 0, 4}, {1, 0, 0}}; + Spec spec = {crash_nums, 1, crash_inputs, 1, NULL, 0, handlers, 2, code, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, + NULL, 0, NULL, 0}; + CHECK(load(spec, &module_a) == PURE_OK); + PureApp app; + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + for (int i = 0; i < 5; ++i) CHECK(pure_app_frame(&app) == PURE_OK); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 0); + for (int i = 0; i < 3; ++i) CHECK(pure_app_frame(&app) == PURE_OK); + CHECK(pure_app_save(&app) == PURE_OK); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 3); + for (int i = 0; i < 2; ++i) CHECK(pure_app_frame(&app) == PURE_OK); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 5); + pure_app_stop(&app); + remove_store(path); +} + +static bool list_keys(void *context, const char *key) { + strcat(context, key); + strcat(context, ","); + return true; +} + +/* A message's document and the states its handlers change commit together. */ +static void stored_messages(void) { + char *path = new_store(); + CHECK(load(editor_spec, &module_a) == PURE_OK); + PureApp app; + unsigned char buffer[64]; + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "hello")) == PURE_OK && app.revision == 1); + CHECK(app.values[0] == 1); + CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "x")) == PURE_CONFLICT && app.values[0] == 1); + /* An edit past the size guard fails: neither the document nor the count moves. */ + CHECK(pure_app_message(&app, 3, "doc", 1, edit_payload(buffer, 5, 0, "-world")) == PURE_LIMIT && app.values[0] == 1); + CHECK(pure_app_message(&app, 3, "doc", 1, edit_payload(buffer, 5, 0, "!")) == PURE_OK && app.values[0] == 2); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0] == 2); + CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && app.revision == 2 && replied(&app, "hello!")); + pure_app_stop(&app); + /* Documents stay documents: the app's states are not among the keys. */ + char seen[256] = ""; + CHECK(pure_store_keys(path, list_keys, seen) == PURE_OK && !strcmp(seen, "doc,")); + remove_store(path); +} + +/* A stored list whose record changed keeps each field by name, when the app + * starts from it and when its module is replaced; stored values that no + * longer fit fall back, and damaged stored values stop the start. */ +static void stored_migration(void) { + char *path = new_store(); + static const RF changed_fields[] = {{"done", 0, 0}, {"note", 1, 1}, {"title", 1, 0}, {"rank", 0, 5}}; + static const R changed[] = {{"task", changed_fields, 4}}; + Spec before = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0, + crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0}; + Spec after = before; + after.r = changed; + after.h = NULL; + after.nh = 0; + CHECK(load(before, &module_a) == PURE_OK && load(after, &module_b) == PURE_OK); + PureApp app; + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); + pure_app_stop(&app); + for (int round = 0; round < 2; ++round) { /* migrated, then stored as migrated */ + CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && pure_list_count(app.lists[TASKS]) == 2); + PureCell note = pure_list_get(app.lists[TASKS], 1, 1), title = pure_list_get(app.lists[TASKS], 1, 2); + CHECK(pure_list_get(app.lists[TASKS], 1, 0).num == 2 && pure_list_get(app.lists[TASKS], 1, 3).num == 5); + CHECK(note.size == 1 && note.text[0] == 'x' && title.size == 1 && title.text[0] == 'x'); + CHECK(app.values[0] == 2 && app.values[4] == 1 && pure_draft_size(app.texts[NOTE]) == 2); + pure_app_stop(&app); + } + /* Back to the first record by replacing the module; the next save stores it. */ + CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK); + CHECK(pure_app_replace(&app, &module_a) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && consistent(&app) == 3); + pure_app_stop(&app); + /* Stored numbers that no longer fit their state start again. */ + PureDraft *values; + int64_t revision; + CHECK(pure_state_open(path, "values", &values) == PURE_OK); + unsigned char record[64] = "PVS1"; + size_t at = 4; + const char *names[] = {"at", "i", "n"}; + double stored[] = {100, 7.5, 9}; + uint8_t types[] = {PURE_STATE_CURSOR, PURE_STATE_INDEX, PURE_STATE_NUM}; + for (unsigned k = 0; k < 3; ++k) { + record[at++] = types[k]; + record[at++] = (unsigned char)strlen(names[k]); + memcpy(record + at, names[k], strlen(names[k])); + at += strlen(names[k]); + uint64_t bits; + memcpy(&bits, &stored[k], sizeof(bits)); + for (unsigned b = 0; b < 8; ++b) record[at++] = (unsigned char)(bits >> (8 * b)); + } + CHECK(pure_draft_splice(values, 0, pure_draft_size(values), (PureSpan){record, at}) == PURE_OK); + CHECK(pure_draft_commit(values, &revision) == PURE_OK); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(app.values[0] == 9 && app.values[2] == 0 && app.values[4] == 0); + pure_app_stop(&app); + /* Damaged stored lists and texts are refused, not replaced. */ + const char *damaged[] = {"list-tasks", "text-note"}; + for (unsigned k = 0; k < 2; ++k) { + PureDraft *draft; + CHECK(pure_state_open(path, damaged[k], &draft) == PURE_OK); + CHECK(pure_draft_splice(draft, 0, 0, (PureSpan){(const unsigned char *)"\xff", 1}) == PURE_OK); + CHECK(pure_draft_commit(draft, &revision) == PURE_OK); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_STORE_ERROR); + CHECK(pure_draft_splice(draft, 0, 1, (PureSpan){0}) == PURE_OK && pure_draft_commit(draft, &revision) == PURE_OK); + pure_draft_close(draft); + } + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(pure_list_count(app.lists[TASKS]) == 3 && pure_draft_size(app.texts[NOTE]) == 3 && app.values[0] == 9); + pure_app_stop(&app); + pure_draft_close(values); + remove_store(path); +} + +static void stored_states(void) { + static const char *const list_words[] = {"a", "\xea\xb0\x80", "", "\n", "\xf0\x9f\x98\x80"}; + static const char *const text_words[] = {"a", "\xea\xb0\x80", "\n", "\xf0\x9f\x98\x80", "xy\nz", ""}; + stored_equivalence(list_spec, list_words, LEN(list_words), 0x9e3779b9u); + stored_equivalence(text_spec, text_words, LEN(text_words), 0x7f4a7c15u); + stored_crashes(); + stored_frames(); + stored_messages(); + stored_migration(); +} + int main(void) { behaviour(); faults(); @@ -1275,7 +1587,8 @@ int main(void) { list_mutations(); tables(); table_mutations(); + stored_states(); printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp)); - puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide\"}"); + puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-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\"}"); return 0; } -- cgit v1.2.3