diff options
| author | eunhhu <[email protected]> | 2026-10-04 13:30:31 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-04 13:30:31 +0100 |
| commit | d806c3197500565d20787161d1e1e94ce39df7a1 (patch) | |
| tree | 26664778482aa7051d89d07c01f79e4527560a8d | |
| parent | f668d54e0c84bdb6240ba1133ee167385a7dc440 (diff) | |
| download | pure-d806c3197500565d20787161d1e1e94ce39df7a1.tar.gz pure-d806c3197500565d20787161d1e1e94ce39df7a1.zip | |
Keep transient states the app's while it runs, never stored (018 stage 1)
| -rw-r--r-- | core/module-save.c | 12 | ||||
| -rw-r--r-- | core/module.c | 69 | ||||
| -rw-r--r-- | core/module.h | 10 | ||||
| -rw-r--r-- | core/test-module.c | 91 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 11 | ||||
| -rw-r--r-- | docs/design/018-lifetimes-and-computation.md | 7 | ||||
| -rw-r--r-- | tests/check.py | 91 |
7 files changed, 266 insertions, 25 deletions
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 ---------------------------------------------------- */ diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index de5849a..e9fe3ca 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -33,7 +33,7 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): | 16 | u32 명령 수, u32 문자열 바이트 수 | | 24 | u16 필드, u16 컬렉션, u16 상수, u16 레코드 | | 32 | u32 상수 블롭 바이트 수, u16 레코드 필드, u16 0 | -| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스, 5 bool, 6 i64), 8 B 값: 수와 bool은 f64 초기값(bool은 0이나 1), i64는 초기값(부호 있는 little-endian), text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다), 인덱스는 u32 대상 리스트 상태 | +| 40 | 상태 × 16 B: u32 이름, u32 타입(비트 0–7: 0 수, 1 text, 2 커서, 3 리스트, 4 인덱스, 5 bool, 6 i64. 비트 8: 저장하지 않는 상태(transient, [018](018-lifetimes-and-computation.md)), 그 위는 0. 메시지의 행 목록은 transient가 아니고, transient text나 리스트를 가리키는 커서와 인덱스는 transient다), 8 B 값: 수와 bool은 f64 초기값(bool은 0이나 1), i64는 초기값(부호 있는 little-endian), text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다), 인덱스는 u32 대상 리스트 상태 | | | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) | | | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text, 4 타이머, 5 출력: 앱이 보내는 메시지, 모양은 메시지와 같고 핸들러가 없다, 6 인자: 한 보기가 받는 인자, 모양은 메시지와 같고 핸들러가 없으며 이름은 `보기.args`), u8 키별 여부, u16 0, u32 키 이름(키 입력)·주기(타이머, 1 ms–24시간을 ms로)·보기 번호(인자) 또는 0, u16 첫 필드, u16 필드 수 | | | 필드 × 8 B: u32 이름(식별자, 또는 그룹 안의 필드면 점으로 이은 경로 `user.name`), u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text, 5 행: 비트 8–23에 그 행이 채우는 행 목록 상태, 6 bool: 1바이트 0이나 1, 7 i64: 8바이트 부호 있는 little-endian). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) | @@ -275,7 +275,10 @@ text·리스트가 남을 때만 위치를 유지한다. ## 상태 영속 데이터 호스트가 상태를 맡으면(`PureData`의 `state`·`save`, 트리 저장소에서는 `pure_tree_data`) 앱의 모든 상태가 -저장소에 산다. 앱에 저장 코드가 없다. 선언한 상태가 곧 영속 상태다. +저장소에 산다. 앱에 저장 코드가 없다. 선언한 상태가 곧 영속 상태다. 다만 transient 상태(타입 워드의 비트 8, 018)는 앱이 +도는 동안만 있다: 저장층은 그것을 열지도 쓰지도 않고(값 묶음, text 초안, 리스트의 이미지와 로그 모두), 시작할 때마다 초기값이다. +실패한 이벤트는 그것도 되돌린다. 그것만 바꾼 이벤트의 저장은 아무것도 쓰지 않는다. 스냅숏은 transient text를 복사한다(저장소에 +없으니 매핑을 나눌 수 없다). | 상태 | 저장 형태 (런타임 이름공간, 문서 키와 겹치지 않음) | |---|---| @@ -323,7 +326,9 @@ text·리스트가 남을 때만 위치를 유지한다. Pi 5의 SD(ext4)에서 이벤트 600개: 이벤트마다 저장 6,282 ms(10.47 ms/이벤트), 8개씩 973 ms, 64개씩 341 ms, 한 번에 194 ms. `pure-run --store 경로 --batch N`으로 잰다. - **교체.** 모듈을 바꾸면 새 상태는 저장된 값이 있으면 그것으로 시작하고, 버린 상태의 저장본은 그대로 남는다. - 새 모듈의 상태는 다음 이벤트나 저장에서 공개된다. + 새 모듈의 상태는 다음 이벤트나 저장에서 공개된다. 이어지는 상태가 transient가 되면 저장소에는 버린 상태처럼 남는다 + (로그에만 있던 리스트의 변경은 이미지로 쓴다). transient였던 상태가 저장되는 상태가 되면 리스트는 교체 때 이미지를 쓰고, + 수와 text는 다음 저장에서 쓰인다. - **스냅숏(다른 스레드의 읽기).** 저장 직후 `pure_app_snapshot`은 저장된 상태의 읽기 전용 스냅숏을 만든다. 수·커서· 인덱스는 복사하고, text 상태는 저장소 매핑의 그 버전을 가리키는 얼린 draft(`pure_draft_freeze`, 복사 없음)로, 리스트는 항목을 공유하는 사본(항목 포인터 배열만 복사, 바뀌지 않은 리스트는 앞 스냅숏의 사본)으로 둔다. 공유된 항목은 diff --git a/docs/design/018-lifetimes-and-computation.md b/docs/design/018-lifetimes-and-computation.md index 901ab08..fab6d96 100644 --- a/docs/design/018-lifetimes-and-computation.md +++ b/docs/design/018-lifetimes-and-computation.md @@ -1,6 +1,6 @@ # 018 — 상태의 수명과 메모리 안의 계산 -작성일: 2026-10-04. 상태: 설계와 착수 기준(구현 전). +작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계(transient) 구현, 검사로 고정했다. [외부 리뷰 005](../outside-feedback/feedback-005.md)의 (1) "순수 인메모리 연산 및 알고리즘의 단절"과, 상태의 영속성과 상태성을 aup 안에서 정할 수단이 필요하다는 요청에서 시작한다. @@ -81,7 +81,10 @@ view distance(from: u32, to: u32) = { # 보기의 본문(3단계): 스냅숏� (위의 `picked`). - 모듈을 바꿀 때 저장되던 상태가 transient가 되면 그 값은 메모리에서 이어지고, 저장소의 옛 값은 모듈이 버린 상태의 값처럼 남는다(지금도 버린 상태는 지우지 않는다). 다시 저장되는 상태가 되면 그때 시작하는 앱은 저장소의 그 값을 받는다(버렸던 - 상태를 다시 들인 것과 같다). transient였던 것이 저장되는 상태가 되면 바꾸는 그 저장에서 지금의 값이 쓰인다. + 상태를 다시 들인 것과 같다). transient였던 것이 저장되는 상태가 되면 리스트는 바꿀 때 이미지를 쓰고, 수와 text는 바꾼 뒤의 + 첫 저장에서 쓰인다. +- 스냅숏(다른 스레드의 읽기)은 저장되는 text를 저장소의 매핑으로 나누지만 transient text는 저장소에 없으니 스냅숏마다 복사한다. + 큰 text를 transient로 두면 그만큼 든다. - **`let 이름: list of 레코드 [by 필드]`** 는 이벤트의 리스트다. 핸들러(3단계부터는 보기 본문)의 맨 바깥 블록에만 둔다. 저장되는 리스트가 하는 것을 모두 한다(`add`, `append`, `remove`, `[키]`, `has`, `count`, `first`, `last`, `+ n`, 집계, 걷기). 이벤트가 시작할 때 비어 있고, 끝나면(성공이든 실패든) 비워진다. 되돌리기를 기록하지 않는다. diff --git a/tests/check.py b/tests/check.py index af93b82..adb2f15 100644 --- a/tests/check.py +++ b/tests/check.py @@ -4271,6 +4271,96 @@ on put { """ +TRANSIENT_SOURCE = """record Hit { + n = 0 +} +state count = 0 +transient state online = 0 +transient state recent: list of Hit +state hits: list of Hit +transient state status: text = "idle" +input tap = text +input peek = text +input say = text +on say { + status = say +} +on tap { + count = count + 1 + online = online + 1 + recent.append(Hit { n: online }) + hits.append(Hit { n: count }) +} +on peek { + online = online + 10 + recent.append(Hit { n: online }) +} +view total = count +view live = online +view seen = recent.count +view kept = hits.count +view said = status +""" + + +def server_transient(aup, pure, work): + """Transient states (docs/design/018) are served as any other, and + never stored: requests that change only them write nothing to the store, + a store written with them between the same stored events is byte for + byte the one written without, and a host killed and started again begins + them anew while the stored states are back.""" + import signal + module, source = work / 'transient.pmod', work / 'transient.aup' + source.write_text(TRANSIENT_SOURCE) + compile_module(aup, source, module) + + def files(store): + return {str(p.relative_to(store)): p.read_bytes() for p in sorted(store.rglob('*')) if p.is_file()} + + def views(port): + return {v: int(http(port, 'GET', f'/views/{v}')[3]) for v in ('total', 'live', 'seen', 'kept')} + + plain, mixed = work / 'transient-plain', work / 'transient-mixed' + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', plain]) + try: + for _ in range(5): + assert http(host.port, 'POST', '/inputs/tap', b'x')[0] == 200 + finally: + host.stop() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', mixed]) + quiet = 0 + try: + for k in range(5): + before = files(mixed) + for _ in range(20): + assert http(host.port, 'POST', '/inputs/peek', b'x')[0] == 200 + assert http(host.port, 'POST', '/inputs/say', f'busy {k}'.encode())[0] == 200 + assert http(host.port, 'GET', '/views/said')[3] == f'busy {k}'.encode() # read from a snapshot + assert files(mixed) == before, k # only transient states changed: nothing written + quiet += 20 + assert http(host.port, 'POST', '/inputs/tap', b'x')[0] == 200 + assert views(host.port) == {'total': 5, 'live': 1005, 'seen': 105, 'kept': 5}, views(host.port) + finally: + host.stop() + stored_plain, stored_mixed = files(plain), files(mixed) + assert stored_plain.keys() == stored_mixed.keys() and stored_plain == stored_mixed, \ + [name for name in stored_plain if stored_plain[name] != stored_mixed.get(name)] + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', mixed]) + assert views(host.port) == {'total': 5, 'live': 0, 'seen': 0, 'kept': 5}, views(host.port) + assert http(host.port, 'GET', '/views/said')[3] == b'idle' + for _ in range(3): + assert http(host.port, 'POST', '/inputs/peek', b'x')[0] == 200 + assert http(host.port, 'POST', '/inputs/tap', b'x')[0] == 200 + host.process.send_signal(signal.SIGKILL) + host.process.communicate(timeout=30) + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', mixed]) + try: + assert views(host.port) == {'total': 6, 'live': 0, 'seen': 0, 'kept': 6}, views(host.port) + finally: + host.stop() + return {'quiet_requests': quiet, 'store_files_compared': len(stored_plain)} + + def server_tokens(aup, pure, work): """An app's routes take tokens (docs/design/017, 3b). With a tokens file an app answers no request without a token (401, WWW-Authenticate: @@ -4494,6 +4584,7 @@ def main(): report['server_apps_links'] = server_apps_links(aup, pure, work) report['server_apps_lifecycle'] = server_apps_lifecycle(aup, pure, work) report['server_tokens'] = server_tokens(aup, pure, work) + report['server_transient'] = server_transient(aup, pure, work) print(json.dumps(report)) |
