From d806c3197500565d20787161d1e1e94ce39df7a1 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 4 Oct 2026 13:30:31 +0100 Subject: Keep transient states the app's while it runs, never stored (018 stage 1) --- core/module-save.c | 12 ++++--- core/module.c | 69 +++++++++++++++++++++++++++++++++-------- core/module.h | 10 ++++++ core/test-module.c | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 162 insertions(+), 20 deletions(-) (limited to 'core') diff --git a/core/module-save.c b/core/module-save.c index 1300832..7d9d951 100644 --- a/core/module-save.c +++ b/core/module-save.c @@ -260,7 +260,8 @@ static int read_outputs(const unsigned char *at, size_t size, SentShape *shapes, static PureList *stored_list(const PureApp *app, uint32_t i) { if (i == PURE_LIST_HEARD) return app->heard; if (i == PURE_LIST_OUTBOX) return app->outbox; - return i < app->module->states && app->module->state_type[i] == PURE_STATE_LIST ? app->lists[i] : NULL; + return i < app->module->states && app->module->state_type[i] == PURE_STATE_LIST && + !pure_state_transient(app->module, i) ? app->lists[i] : NULL; } static const char *stored_name(const PureApp *app, uint32_t i) { @@ -696,7 +697,7 @@ static size_t encode_values(const PureModule *m, const PureValue *values, unsign 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; + if (!scalar(m->state_type[i]) || pure_state_transient(m, i)) continue; size_t length = strlen(m->state_names[i]); uint64_t bits; memcpy(&bits, &values[i], sizeof(bits)); @@ -715,7 +716,7 @@ PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, Pu PureDraft *const *texts, PureList *const *lists) { bool scalars = false, fresh = false; for (uint32_t i = 0; i < m->states; ++i) - if (scalar(m->state_type[i])) { + if (scalar(m->state_type[i]) && !pure_state_transient(m, i)) { scalars = true; fresh = fresh || kept[i] < 0; } @@ -734,7 +735,7 @@ PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, Pu for (unsigned b = 0; b < 8; ++b) bits |= (uint64_t)bytes[at + b] << (8 * b); at += 8; int state = pure_module_state(m, name); - if (state < 0 || kept[state] >= 0 || !scalar(type)) continue; + if (state < 0 || kept[state] >= 0 || !scalar(type) || pure_state_transient(m, (uint32_t)state)) continue; /* A number, a bool or an i64, each as the state's type holds it when it fits (pure_cell_as); a position where it fits. */ uint8_t from = pure_value_kind(type), to = pure_value_kind(m->state_type[state]); PureCell cell = {.i64 = (int64_t)bits}; @@ -1016,7 +1017,8 @@ PureResult pure_save_commit(PureApp *app, const PureValue *values, void *const * uint32_t states[2 * PURE_MODULE_STATES + 3]; 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] : + PureDraft *draft = pure_state_transient(m, i) ? NULL : + 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; diff --git a/core/module.c b/core/module.c index f3fbcd2..6df93bc 100644 --- a/core/module.c +++ b/core/module.c @@ -1035,8 +1035,10 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule const unsigned char *text = p + size - constant_bytes - strings; if (constant_bytes) memcpy(out->constant_data, p + size - constant_bytes, constant_bytes); for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) { - out->state_type[i] = (uint8_t)read32(at + 4); - if (read32(at + 4) > PURE_STATE_I64) REFUSE("state type", i); + uint32_t type = read32(at + 4); + out->state_type[i] = (uint8_t)type; + if ((type & ~(uint32_t)PURE_STATE_TRANSIENT) > PURE_STATE_I64) REFUSE("state type", i); + if (type & PURE_STATE_TRANSIENT) out->transient |= UINT32_C(1) << i; /* A number's, a bool's or an i64's value is its 8 bytes; others have flags: * a mark (cursor), a keyed list's key field + 1, or a list of a message's rows. */ bool value = out->state_type[i] == PURE_STATE_NUM || out->state_type[i] == PURE_STATE_BOOL || @@ -1044,6 +1046,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule uint32_t flags = value ? 0 : read32(at + 12); bool cursor = out->state_type[i] == PURE_STATE_CURSOR, list = out->state_type[i] == PURE_STATE_LIST; bool rows = list && flags == PURE_LIST_ROWS; + if (rows && type & PURE_STATE_TRANSIENT) REFUSE("state type", i); /* an event's own, saved or not */ /* A list of a message's rows is named by the field's path (input.field), no other state. */ if (!name_at(text, strings, read32(at), rows ? NAME_PATH : NAME_IDENTIFIER, out->state_names[i]) || (rows && !strchr(out->state_names[i], '.'))) @@ -1182,6 +1185,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_BOOL))) || (out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST))) REFUSE("state reference", i); + /* A stored position into what is not stored would come back meaning nothing. */ + for (uint32_t i = 0; i < out->states; ++i) + if ((out->state_type[i] == PURE_STATE_CURSOR || out->state_type[i] == PURE_STATE_INDEX) && + !(out->transient >> i & 1) && out->transient >> out->state_ref[i] & 1) + REFUSE("stored position in a transient state", i); /* A table names its list in place of code; its columns follow it. */ for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) { if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->view_names[i])) REFUSE("view name", i); @@ -2791,6 +2799,30 @@ PURE_COLD static PureResult new_text(PureDraft **out, const PureModule *m, uint3 return result; } +/* A stored draft given a transient text's bytes, to be saved (saved: none yet). */ +PURE_COLD static PureResult copy_text(PureDraft *to, const PureDraft *from, PureVersion *saved) { + size_t size = pure_draft_size(from); + unsigned char *bytes = size ? malloc(size) : NULL; + if (size && !bytes) return PURE_ALLOCATION; + Copy copy = {bytes}; + PureResult result = size ? pure_draft_read(from, 0, size, copy_out, ©) : PURE_OK; + if (result == PURE_OK) result = pure_draft_splice(to, 0, pure_draft_size(to), (PureSpan){bytes, size}); + free(bytes); + *saved = (PureVersion){UINT64_MAX, UINT64_MAX}; + return result; +} + +/* A draft in memory with a text's bytes, or NULL. */ +static PureDraft *text_copy(const PureDraft *from) { + PureDraft *copy = pure_draft_new(); + PureVersion unused; + if (copy && copy_text(copy, from, &unused) != PURE_OK) { + pure_draft_close(copy); + copy = NULL; + } + return copy; +} + PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record) { const PureRecord *r = &m->record[record]; uint8_t kinds[PURE_RECORD_FIELDS]; @@ -3674,7 +3706,10 @@ PureSnapshot *pure_save_snapshot(PureApp *app, uint32_t runtime) { memcpy(made->values, app->values, sizeof(made->values)); bool ok = true; for (uint32_t i = 0; i < m->states && ok; ++i) { - if (app->texts[i]) ok = (made->texts[i] = pure_draft_freeze(app->texts[i])) != NULL; + /* A stored text is shared as its store holds it; a transient one, never stored, is copied. */ + if (app->texts[i]) + ok = (made->texts[i] = pure_state_transient(m, i) ? text_copy(app->texts[i]) : pure_draft_freeze(app->texts[i])) != + NULL; if (!ok || !app->lists[i]) continue; if (last && last->lists[i] && pure_list_current(last->lists[i], app->lists[i])) { pure_list_hold(last->lists[i]); @@ -3921,22 +3956,29 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) bool valued = pure_value_kind(was) != 0xFF && pure_value_kind(now) != 0xFF; kept[i] = previous >= 0 && (was == now || valued) ? previous : -1; if (kept[i] >= 0) saved[i] = app->saved[kept[i]]; + /* A transient state is never stored (018): one that was stored is now as a dropped one is. */ + bool transient = pure_state_transient(module, i), was_transient = kept[i] >= 0 && pure_state_transient(old, kept[i]); if (module->state_type[i] == PURE_STATE_TEXT) { - if (kept[i] >= 0) texts[i] = app->texts[kept[i]]; + if (kept[i] >= 0 && (transient || !was_transient || !stored)) texts[i] = app->texts[kept[i]]; + else if (result == PURE_OK && kept[i] >= 0 && /* a text stored from now on: its draft in the store holds it */ + (result = pure_save_open_text(app, module, i, &texts[i], &saved[i])) == PURE_OK) + result = copy_text(texts[i], app->texts[kept[i]], &saved[i]); else if (result == PURE_OK) - result = stored ? pure_save_open_text(app, module, i, &texts[i], &saved[i]) : + result = stored && !transient ? 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; bool rows = module->state_flags[i] & PURE_LIST_ROWS >> 8; /* rows are never stored */ + bool saving = lists_stored && !rows && !transient; if (kept[i] >= 0) { result = keep_list(&lists[i], app->lists[kept[i]], old, old->state_ref[kept[i]], module, module->state_ref[i]); list_drafts[i] = app->list_drafts[kept[i]]; - listed[i] = (PureSaved){app->image_seq[kept[i]], app->logged_since[kept[i]], app->logged[kept[i]], false}; - /* Its record changed: its image, in the new fields, holds it before any change is logged. */ - rewrite[i] = result == PURE_OK && lists_stored && !rows && lists[i] != app->lists[kept[i]]; - } else if (lists_stored && !rows) { + if (!transient && !was_transient) + listed[i] = (PureSaved){app->image_seq[kept[i]], app->logged_since[kept[i]], app->logged[kept[i]], false}; + /* Its record changed, or it is stored from now on: its image, in the new fields, holds it before any change is logged. */ + rewrite[i] = result == PURE_OK && saving && (lists[i] != app->lists[kept[i]] || was_transient); + } else if (saving) { result = pure_save_open_list(app, module, i, &lists[i], &list_drafts[i], &listed[i]); rewrite[i] = result == PURE_OK && listed[i].rewrite; } else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION; @@ -3963,7 +4005,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) } if (kept[i] < 0 || lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]); lists[i] = sorted; - rewrite[i] = lists_stored && !(module->state_flags[i] & PURE_LIST_ROWS >> 8); + rewrite[i] = lists_stored && !(module->state_flags[i] & PURE_LIST_ROWS >> 8) && !pure_state_transient(module, i); } /* A cursor or index keeps its position only while its text or list is the * one it belonged to. */ @@ -3999,7 +4041,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) bool imaged = result == PURE_OK; for (uint32_t j = 0; result == PURE_OK && old && j < old->states; ++j) { bool dropped = old->state_type[j] == PURE_STATE_LIST && app->logged_since[j]; - for (uint32_t i = 0; dropped && i < module->states; ++i) dropped = kept[i] != (int)j; + for (uint32_t i = 0; dropped && i < module->states; ++i) dropped = kept[i] != (int)j || pure_state_transient(module, i); if (dropped) result = pure_save_image(app, old, j, app->lists[j]); } for (uint32_t i = 0; i < module->states && result == PURE_OK; ++i) @@ -4022,6 +4064,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) for (uint32_t i = 0; i < module->states; ++i) { if (kept[i] >= 0) { if (lists[i] && lists[i] != app->lists[kept[i]]) pure_list_free(lists[i]); + if (texts[i] && texts[i] != app->texts[kept[i]]) pure_draft_close(texts[i]); continue; } pure_draft_close(texts[i]); @@ -4063,9 +4106,9 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) } pure_save_release(app); for (uint32_t i = 0; i < module->states; ++i) { - /* A stored list's changes are recorded from now on. */ + /* A stored list's changes are recorded from now on; a transient one's never. */ if (lists_stored && module->state_type[i] == PURE_STATE_LIST && !(module->state_flags[i] & PURE_LIST_ROWS >> 8)) - pure_list_journal(lists[i], true); + pure_list_journal(lists[i], !pure_state_transient(module, i)); /* Without memory for its keys a keyed list is still searched, only slower. */ if (module->state_type[i] == PURE_STATE_LIST) (void)pure_list_keyed(lists[i], module->state_key[i] ? (int)module->state_key[i] - 1 : -1); diff --git a/core/module.h b/core/module.h index 162a8b1..d0a82bc 100644 --- a/core/module.h +++ b/core/module.h @@ -144,6 +144,12 @@ typedef union { double num; int64_t i64; } PureValue; * message's rows field fills it as the message arrives, it is emptied when * every event ends, and it is never saved nor logged for undoing. */ enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256 }; +/* A transient state (bit 8 of its type word in a module, a bit of + * PureModule.transient): the app holds it while it runs, and undoes it with + * a failed event, but never saves it: each start begins it at its initial + * value (docs/design/018). A stored cursor or index is of a stored text or + * list. */ +enum { PURE_STATE_TRANSIENT = 256 }; /* A table view has one row per item of a list, or per item its filter * keeps; its columns are number, bool, i64 or text views evaluated for one * row at a time. */ @@ -217,6 +223,7 @@ typedef struct { uint8_t state_type[PURE_MODULE_STATES]; uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */ uint8_t state_flags[PURE_MODULE_STATES]; /* cursor: PURE_CURSOR_STAYS; list: PURE_LIST_ROWS >> 8 */ + uint32_t transient; /* a bit per transient state */ /* A keyed list: its key field + 1 (0: none). Its items stay in key order * (numbers by value, texts by bytes), each key once, and keys do not change. */ uint8_t state_key[PURE_MODULE_STATES]; @@ -248,6 +255,9 @@ typedef struct { PureValue record_field_default[PURE_MODULE_RECORD_FIELDS]; /* a number (a bool's 0 or 1), an i64, or a text constant's index */ } PureModule; +/* Whether a state of m is transient (PURE_STATE_TRANSIENT). */ +static inline bool pure_state_transient(const PureModule *m, uint32_t state) { return m->transient >> state & 1; } + /* Decode and verify into out, which must be zeroed (a static one is) or hold * a loaded module: what it held is given back first. On failure out holds * nothing and error names the rule. */ diff --git a/core/test-module.c b/core/test-module.c index c91bd2c..2f429f1 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -52,6 +52,7 @@ typedef struct { uint8_t op; uint32_t arg; double value; } O; * 32-bit two's complement) states follow the number states; text views * follow the number views. */ typedef struct { const char *name; uint32_t type, ref; } T; /* type: the state type, its flags from bit 8 */ +#define TR (1u << 24) /* in a T's type: a transient state */ /* A record field: a number and its default, text (text = 1) whose default * is the constant numbered initial, a bool (text = 2) or an i64 (text = 3) * and its default. */ @@ -130,7 +131,7 @@ static size_t build(unsigned char *out, Spec spec) { for (size_t i = 0; i < spec.ns; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put_double(p, spec.s[i].initial); } for (size_t i = 0; i < spec.nt; ++i) { p = put32(p, offset[n++]); - p = put32(p, spec.t[i].type & 0xFF); + p = put32(p, (spec.t[i].type & 0xFF) | (spec.t[i].type & TR ? PURE_STATE_TRANSIENT : 0)); if ((spec.t[i].type & 0xFF) == PURE_STATE_BOOL) { p = put_double(p, spec.t[i].ref); continue; } if ((spec.t[i].type & 0xFF) == PURE_STATE_I64) { int64_t initial = (int32_t)spec.t[i].ref; @@ -139,7 +140,7 @@ static size_t build(unsigned char *out, Spec spec) { continue; } p = put32(p, spec.t[i].ref); - p = put32(p, spec.t[i].type >> 8); + p = put32(p, (spec.t[i].type & ~TR) >> 8); } for (size_t i = 0; i < spec.nc; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, 1); } uint32_t field_offsets[64], field_start = 0; @@ -2045,6 +2046,91 @@ static void stored_migration(void) { extern unsigned long pure_save_test_checkpoints; +/* Transient states (018) are the app's while it runs, undone with a failed + * event, never stored: a start begins them anew. Replacing the module moves + * a state between stored and transient: one stored from then on is saved, + * and one no longer stored is to the store as a dropped state. */ +static void transient_states(void) { + enum { N, T_, NOTE_, LOG, I_, KEEP, PICK, REQ = PURE_OP_REQUIRE, TSET_ = PURE_OP_TEXT_SET }; + static const S nums[] = {{"n", 0}}; + static const T a_states[] = {{"t", 0 | TR, 0}, {"note", 1 | TR, 1}, {"log", 3 | TR, 0}, {"i", 4 | TR, LOG}, + {"keep", 3, 0}, {"pick", 4 | TR, KEEP}}; + static const T b_states[] = {{"t", 0, 0}, {"note", 1, 1}, {"log", 3, 0}, {"i", 4 | TR, LOG}, + {"keep", 3 | TR, 0}, {"pick", 4 | TR, KEEP}}; + static const T stored_index[] = {{"t", 0 | TR, 0}, {"note", 1 | TR, 1}, {"log", 3 | TR, 0}, {"i", 4, LOG}, + {"keep", 3, 0}, {"pick", 4 | TR, KEEP}}; + static const RF fields[] = {{"value", 0, 0}}; + static const R records[] = {{"row", fields, 1}}; + static const char *const constants[] = {"", "x"}; + static const I inputs[] = {KEY("inc", "KeyI"), KEY("fail", "KeyF"), KEY("touch", "KeyT")}; + static const O code[] = { + {L, N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, N, 0}, /* n = n + 1 */ + {L, T_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, T_, 0}, /* t = t + 1 */ + {L, T_, 0}, {LA, LOG, 0}, {L, N, 0}, {LA, KEEP, 0}, /* log.append(row { t }), keep.append(row { n }) */ + {XE, LOG, 0}, {XS, I_, 0}, {XE, KEEP, 0}, {XS, PICK, 0}, /* i = log.last, pick = keep.last */ + {TL, NOTE_, 0}, {TK, 1, 0}, {CAT, 0, 0}, {TSET_, NOTE_, 0}, /* note = note + "x" */ + /* fail: the transient changes, then a require that fails */ + {L, T_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, T_, 0}, {L, T_, 0}, {LA, LOG, 0}, + {TL, NOTE_, 0}, {TK, 1, 0}, {CAT, 0, 0}, {TSET_, NOTE_, 0}, {C, 1, 0}, {REQ, 0, 0}, + /* touch: only transient states */ + {L, T_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, T_, 0}, {L, T_, 0}, {LA, LOG, 0}, + }; + static const H handlers[] = {{1, 0, 20}, {3, 20, 12}, {5, 32, 6}}; + Spec a = {nums, 1, inputs, 3, NULL, 0, handlers, 3, code, LEN(code), NULL, 0, a_states, 6, constants, 2, NULL, 0, + records, 1, NULL, 0}; + Spec b = a, c = a; + b.t = b_states; + c.t = stored_index; + PureModule refused = {0}; + CHECK(load(c, &refused) != PURE_OK && strstr(error, "stored position in a transient state")); + CHECK(load(a, &module_a) == PURE_OK && load(b, &module_b) == PURE_OK); + CHECK(pure_state_transient(&module_a, T_) && !pure_state_transient(&module_a, KEEP) && !pure_state_transient(&module_b, T_)); + char *path = new_store(); + 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); + CHECK(app.values[N].num == 2 && app.values[T_].num == 2 && pure_list_count(app.lists[LOG]) == 2 && + pure_list_count(app.lists[KEEP]) == 2 && app.values[I_].num == 1 && app.values[PICK].num == 1 && + pure_draft_size(app.texts[NOTE_]) == 3); + /* A snapshot copies a transient text, and shares its lists as any other. */ + PureSnapshot *snapshot = pure_app_snapshot(&app); + PureApp reader = {0}; + CHECK(snapshot); + pure_app_read(&reader, snapshot); + CHECK(pure_draft_size(reader.texts[NOTE_]) == 3 && pure_list_count(reader.lists[LOG]) == 2 && reader.values[T_].num == 2); + pure_app_stop(&reader); + pure_snapshot_release(snapshot); + /* A failed event undoes transient states too. */ + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) != PURE_OK && app.values[T_].num == 2 && + pure_list_count(app.lists[LOG]) == 2 && pure_draft_size(app.texts[NOTE_]) == 3); + /* Changing only transient states stores nothing. */ + uint64_t seq = app.seq; + for (int k = 0; k < 3; ++k) CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK); + CHECK(app.seq == seq && app.values[T_].num == 5 && pure_list_count(app.lists[LOG]) == 5); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(app.values[N].num == 2 && pure_list_count(app.lists[KEEP]) == 2 && app.values[T_].num == 0 && + !pure_list_count(app.lists[LOG]) && app.values[I_].num == 0 && app.values[PICK].num == 0 && + pure_draft_size(app.texts[NOTE_]) == 1); + /* Into b: t, note and log are stored from now on, keep is not. */ + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); + CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.values[T_].num == 2 && pure_list_count(app.lists[LOG]) == 2 && + pure_list_count(app.lists[KEEP]) == 4 && pure_draft_size(app.texts[NOTE_]) == 3); + CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); + pure_app_stop(&app); + CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK); + CHECK(app.values[N].num == 5 && app.values[T_].num == 3 && pure_list_count(app.lists[LOG]) == 3 && + pure_list_get(app.lists[LOG], 2, 0).num == 3 && pure_draft_size(app.texts[NOTE_]) == 4 && + !pure_list_count(app.lists[KEEP])); + pure_app_stop(&app); + /* Back in a, keep is stored again: what the store has of it is what it held when it stopped being stored. */ + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK); + CHECK(app.values[N].num == 5 && pure_list_count(app.lists[KEEP]) == 4 && app.values[T_].num == 0 && + !pure_list_count(app.lists[LOG]) && pure_draft_size(app.texts[NOTE_]) == 1); + pure_app_stop(&app); + 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", ""}; @@ -2057,6 +2143,7 @@ static void stored_states(void) { stored_frames(); stored_messages(); stored_migration(); + transient_states(); } /* ---- Deferred saving ---------------------------------------------------- */ -- cgit v1.2.3