From 08056bb65bb9d1cfa4ccb1cd0b9cb67554d560a3 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Fri, 25 Sep 2026 11:57:33 +0100 Subject: Allow drafts without a stored base Runtime-owned values such as an app's text need the same extent tree as stored documents without a store file. pure_draft_new creates such a draft; it edits, lends versions and reclaims like any draft but refuses to commit. pure_draft_restore returns a held draft to an earlier version, so a failed event can abandon its edits. A test runs 4,000 random edits with a fifth abandoned against a flat oracle under ASan/UBSan. --- prototypes/connections/state.h | 5 +++++ prototypes/connections/store-tree.c | 9 ++++++++ prototypes/connections/test-tree-store.c | 37 ++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) (limited to 'prototypes') diff --git a/prototypes/connections/state.h b/prototypes/connections/state.h index d0b386c..461f71a 100644 --- a/prototypes/connections/state.h +++ b/prototypes/connections/state.h @@ -12,6 +12,9 @@ * appends only the draft's new extents. One owner thread per draft. */ typedef struct PureDraft PureDraft; +/* A working value with no stored base, for runtime-owned state such as an + * app's text. It edits and reads like any draft but cannot be committed. */ +PureDraft *pure_draft_new(void); /* A missing key opens as an empty draft at revision 0; commit creates it. */ PureResult pure_draft_open(const char *path, const char *key, PureDraft **out); /* Memory only. Fails without change on a bad range or when memory runs out. */ @@ -32,6 +35,8 @@ PureResult pure_draft_read_version(const PureDraft *, PureVersion, size_t offset bool (*visit)(void *, PureSpan), void *context); /* While held, edits keep every earlier version's nodes (one transaction). */ void pure_draft_hold(PureDraft *, bool held); +/* Abandon edits made since version was taken; the draft was held since then. */ +void pure_draft_restore(PureDraft *, PureVersion); /* Publish if the key is still at the draft's base revision. On success the * draft continues from the new revision; on any failure it is unchanged. */ PureResult pure_draft_commit(PureDraft *, int64_t *revision); diff --git a/prototypes/connections/store-tree.c b/prototypes/connections/store-tree.c index 74763ca..414c8ce 100644 --- a/prototypes/connections/store-tree.c +++ b/prototypes/connections/store-tree.c @@ -987,6 +987,10 @@ PureResult pure_draft_open(const char *path, const char *key, PureDraft **out) { return PURE_OK; } +PureDraft *pure_draft_new(void) { + return calloc(1, sizeof(PureDraft)); /* no path: nothing mapped, nothing to commit */ +} + void pure_draft_close(PureDraft *draft) { if (!draft) return; release_tree(&draft->tree); @@ -1066,6 +1070,10 @@ PureResult pure_draft_read(const PureDraft *draft, size_t offset, size_t length, return pure_draft_read_version(draft, pure_draft_version(draft), offset, length, visit, context); } +void pure_draft_restore(PureDraft *draft, PureVersion version) { + draft->root = (Entry){version.ref, version.bytes}; +} + void pure_draft_hold(PureDraft *draft, bool held) { draft->held = held; if (!held) collect(draft); @@ -1073,6 +1081,7 @@ void pure_draft_hold(PureDraft *draft, bool held) { PureResult pure_draft_commit(PureDraft *draft, int64_t *revision) { *revision = draft->revision; + if (!draft->path) return PURE_INVALID; Session session; Writer *writer = NULL; Entry current, root = draft->root; diff --git a/prototypes/connections/test-tree-store.c b/prototypes/connections/test-tree-store.c index a9aea86..971abba 100644 --- a/prototypes/connections/test-tree-store.c +++ b/prototypes/connections/test-tree-store.c @@ -572,6 +572,39 @@ static void draft_versions(const char *path) { pure_draft_close(draft); } +/* A draft without a store: edits, versions, restore and reclamation. */ +static void anonymous_draft(const char *path) { + (void)path; + Oracle o = {malloc(CAPACITY), 0, 0}; + unsigned char *scratch = malloc(CAPACITY), insert[3000]; + PureDraft *draft = pure_draft_new(); + CHECK(draft && o.bytes && scratch && pure_draft_size(draft) == 0 && pure_draft_revision(draft) == 0); + uint32_t state = 42; + for (unsigned step = 0; step < 4000; ++step) { + size_t count = random_value(&state) % (step % 50 ? 12 : sizeof(insert)); + size_t offset = o.size ? random_value(&state) % (o.size + 1) : 0; + size_t remove = o.size - offset ? random_value(&state) % (o.size - offset + 1) % 32 : 0; + fill(insert, count, &state); + bool abandon = random_value(&state) % 5 == 0; + pure_draft_hold(draft, true); + PureVersion before = pure_draft_version(draft); + CHECK(pure_draft_splice(draft, offset, remove, (PureSpan){insert, count}) == PURE_OK); + if (abandon) pure_draft_restore(draft, before); + else apply(&o, offset, remove, insert, count); + pure_draft_hold(draft, false); + if (step % 37 == 0) expect_draft(draft, &o, scratch); + } + expect_draft(draft, &o, scratch); + int64_t revision; + CHECK(pure_draft_commit(draft, &revision) == PURE_INVALID); + size_t nodes, bytes; + pure_draft_test_arena(draft, &nodes, &bytes); + CHECK(bytes < 2 * o.size + 65536); + pure_draft_close(draft); + free(o.bytes); + free(scratch); +} + /* A failed commit changes neither the store nor the draft. */ static void draft_faults(const char *path) { unsigned char scratch[64]; @@ -621,7 +654,7 @@ int main(void) { void (*checks[])(const char *) = {oracle, amplification, faults, crashes, torn_head, isolation, corruption, many_keys, revision_limit, keys, drafts, typing, draft_conflict, draft_relocation, draft_faults, - draft_versions}; + draft_versions, anonymous_draft}; for (size_t i = 0; i < sizeof(checks) / sizeof(*checks); ++i) { char path[] = "/tmp/pure-tree-check-XXXXXX"; CHECK(mkdtemp(path) && pure_store_init(path) == PURE_OK); @@ -631,6 +664,6 @@ int main(void) { } puts("{\"tree_store\":\"random-oracle-balance-held-views-one-byte-amplification-faults-" "process-crash-torn-head-lockless-isolation-compaction-corruption-1024-keys-revision-limit-key-listing-" - "draft-oracle-typing-arena-conflict-relocation-commit-faults-held-versions\"}"); + "draft-oracle-typing-arena-conflict-relocation-commit-faults-held-versions-anonymous\"}"); return 0; } -- cgit v1.2.3