diff options
Diffstat (limited to 'prototypes/connections')
| -rw-r--r-- | prototypes/connections/Makefile | 12 | ||||
| -rw-r--r-- | prototypes/connections/module-list.c | 117 | ||||
| -rw-r--r-- | prototypes/connections/module-list.h | 11 | ||||
| -rw-r--r-- | prototypes/connections/module-save.c | 291 | ||||
| -rw-r--r-- | prototypes/connections/module-save.h | 30 | ||||
| -rw-r--r-- | prototypes/connections/module-store.c | 21 | ||||
| -rw-r--r-- | prototypes/connections/module-store.h | 5 | ||||
| -rw-r--r-- | prototypes/connections/module.c | 151 | ||||
| -rw-r--r-- | prototypes/connections/module.h | 18 | ||||
| -rw-r--r-- | prototypes/connections/pure-run.c | 21 | ||||
| -rw-r--r-- | prototypes/connections/test-module.c | 317 |
11 files changed, 937 insertions, 57 deletions
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 <math.h> #include <stdlib.h> #include <string.h> @@ -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 <stdbool.h> #include <stddef.h> #include <stdint.h> @@ -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 <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* 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 <math.h> #include <stdio.h> @@ -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 <stdio.h> #include <stdlib.h> #include <string.h> /* 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 <dirent.h> #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/wait.h> +#include <unistd.h> #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; } |
