aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes/connections')
-rw-r--r--prototypes/connections/Makefile12
-rw-r--r--prototypes/connections/module-list.c117
-rw-r--r--prototypes/connections/module-list.h11
-rw-r--r--prototypes/connections/module-save.c291
-rw-r--r--prototypes/connections/module-save.h30
-rw-r--r--prototypes/connections/module-store.c21
-rw-r--r--prototypes/connections/module-store.h5
-rw-r--r--prototypes/connections/module.c151
-rw-r--r--prototypes/connections/module.h18
-rw-r--r--prototypes/connections/pure-run.c21
-rw-r--r--prototypes/connections/test-module.c317
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, &copy)) 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;
}