diff options
| author | eunhhu <[email protected]> | 2026-10-18 19:30:16 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-18 19:30:16 +0100 |
| commit | b22b8c3f6c5378b47c36370225d7c41d4394bb7c (patch) | |
| tree | d1a3cbf1b0e718f0acafa08ec6741761f08e5dd5 | |
| parent | e8b3499289022c8729900a58ed218d961a85fec8 (diff) | |
| download | pure-b22b8c3f6c5378b47c36370225d7c41d4394bb7c.tar.gz pure-b22b8c3f6c5378b47c36370225d7c41d4394bb7c.zip | |
Module format v7, 027's 4b-2: keyed documents gone, a keyed list of records holding bytes doing their work: no collections table, DOC, DOC_SIZE or REPLACE (106 opcodes now 103, those from LEN on moved up), keyed messages or a base revision (pure_app_message takes an input and a payload), no PureData document callbacks (a data host opens states and saves them together), no burst by document key; the header 38 bytes; pure-edit-module gone; pure-serve without /objects, ETag, If-Match and If-None-Match, 412 or an app a thread for a module of documents alone (one without states or outputs runs so); a hole closed: an instruction in no handler's, view's or function's block was never verified yet the load's later passes read its arguments (a damaged module read out of bounds once the header moved what the mutation test damaged), now blanked to CONST 0; checks moved: server_documents (DOCS_SOURCE, keyed byte documents with the app's revision, against a Python model, one and three connection threads, a restart) in place of the parity with editor.pure, pipelining and bursts through kill -9 on its inputs and views, the lent budget on a text view (lending a document's read is server_lent_views' already), clocks and tokens on it; message tests without a document host; the HTTP benchmark's document suite runs the earlier document server and the baselines alone; the count harness builds against either message API (runs.sh build: -DPURE_KEYED_MESSAGES before 4b); counts: b33 against b32 with batches within 0.002% but BFS -0.33%, without batches BFS +0.63%, knapsack +0.52%, join +0.38%; the bank's queries (s32 on a v6 module, s33 on v7) field +0.68%, found +0.44%, large +0.42%, the rest within 0.13%; 008 as v7, 027's 4b-2 as made and 4c designed
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | benchmarks/http/README.md | 13 | ||||
| -rw-r--r-- | benchmarks/http/run.py | 8 | ||||
| -rw-r--r-- | cli/edit-module.c | 179 | ||||
| -rw-r--r-- | cli/pure-run.c | 2 | ||||
| -rw-r--r-- | core/module-save.c | 19 | ||||
| -rw-r--r-- | core/module-save.h | 6 | ||||
| -rw-r--r-- | core/module-store.c | 77 | ||||
| -rw-r--r-- | core/module-store.h | 8 | ||||
| -rw-r--r-- | core/module.c | 214 | ||||
| -rw-r--r-- | core/module.h | 81 | ||||
| -rw-r--r-- | core/test-module.c | 523 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 237 | ||||
| -rw-r--r-- | docs/design/027-computation-core.md | 36 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s32v6.jsonl | 2 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s33.jsonl | 2 | ||||
| -rw-r--r-- | research/experiments/040-against-c/runs.sh | 4 | ||||
| -rw-r--r-- | research/experiments/040-against-c/suite-run.c | 14 | ||||
| -rw-r--r-- | research/experiments/041-batches/2026-10-18-counts.jsonl | 10 | ||||
| -rw-r--r-- | serve/deliver.c | 4 | ||||
| -rw-r--r-- | serve/serve.c | 246 | ||||
| -rw-r--r-- | tests/check.py | 425 |
23 files changed, 730 insertions, 1388 deletions
@@ -32,7 +32,7 @@ OBJECTS = $(patsubst %.c,$(BUILD)/obj/%.o,$(MODEL) $(STORE)) LINK_OBJECTS = $(patsubst %.c,$(BUILD)/obj/%.o,$(LINK)) .PHONY: all check -all: $(BUILD)/libpure.a $(BUILD)/libpure-link.a $(BUILD)/pure-serve $(BUILD)/pure-crypto.so $(BUILD)/pure-run $(BUILD)/pure-edit-module +all: $(BUILD)/libpure.a $(BUILD)/libpure-link.a $(BUILD)/pure-serve $(BUILD)/pure-crypto.so $(BUILD)/pure-run $(BUILD)/obj/%.o: %.c $(HEADERS) @mkdir -p $(dir $@) @@ -79,10 +79,6 @@ $(BUILD)/pure-crypto.so: link/crypto-openssl.c $(HEADERS) $(BUILD)/pure-run: cli/pure-run.c $(BUILD)/libpure.a $(HEADERS) $(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) cli/pure-run.c $(BUILD)/libpure.a $(LDFLAGS) -lm $(LDLIBS) -o $@ -# The document CLI contract, driven by a module on the tree store. -$(BUILD)/pure-edit-module: cli/edit-module.c $(BUILD)/libpure.a $(HEADERS) - $(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) cli/edit-module.c $(BUILD)/libpure.a $(LDFLAGS) -lm $(LDLIBS) -o $@ - # Tests build their own copies: the store's fault hooks exist only under PURE_STORE_TEST. $(BUILD)/test-module: core/test-module.c $(MODEL) $(STORE) $(HEADERS) @mkdir -p $(BUILD) @@ -40,7 +40,7 @@ The source follows the principle below, a small core with the rest selected arou | `store/` | the tree store: one file, a commit is one contiguous write and one sync (formats `PURETRE3`, `PUREREC1`) | in `libpure.a` | | `link/` | links without connections, the default between Pure nodes: saved sends over UDP, sealed with the two nodes' keys, no handshake ([011](docs/design/011-pure-transport.md), specified in [012](docs/design/012-link-protocol.md)); the crypto they and sealed sends use, OpenSSL's part apart, mapped only by a process that uses one | `build/libpure-link.a`, `build/pure-crypto.so` | | `serve/` | `pure-serve`: the HTTP host, outbound delivery, JSON read in place, surfaces for people run in the node ([010](docs/design/010-surfaces.md)) | `build/pure-serve` | -| `cli/` | command-line hosts | `build/pure-run`, `build/pure-edit-module` | +| `cli/` | the command-line test host | `build/pure-run` | | `prototypes/` | the earlier document runtime (000-077), built against `core/` and `store/` | `make -C prototypes/connections` | | `tests/` | the hosts checked end to end, with apps built by aup (the sibling checkout) | `make check` | diff --git a/benchmarks/http/README.md b/benchmarks/http/README.md index cd37579..4c50958 100644 --- a/benchmarks/http/README.md +++ b/benchmarks/http/README.md @@ -1,16 +1,16 @@ # HTTP 벤치마크 -Pure의 문서 서버들과 같은 계약의 axum(Rust)·drogon(C++) 서비스를 같은 기기·코어 배정·부하 생성기로 잰다 -([075](../../docs/research/075-http-host-against-axum.md)). +Pure의 문서 서버와 같은 계약의 axum(Rust)·drogon(C++) 서비스를 같은 기기·코어 배정·부하 생성기로 잰다 +([075](../../docs/research/075-http-host-against-axum.md)). 027의 4b부터 pure-serve에는 키별 문서가 없다(키 있는 리스트가 +담고, 입력과 보기로 내놓는다). 그래서 문서 묶음은 옛 문서 서버(`prototypes/connections`)와 기준선만 돌고, pure-serve는 앱 +묶음에서 잰다. ```sh cc -O2 -std=c17 -o load load.c (cd axum-baseline && cargo build --release) drogon-baseline/build.sh # Debian의 drogon 패키지를 설치하지 않고 .deps에 풀어 빌드한다 -make -C ../.. build/pure-serve make -C ../../prototypes/connections BUILD=build/tree STORE=store-tree.c build/tree/pure-connections -aup build ../../../aup/examples/editor.aup -o editor.pmod -python3 run.py --module editor.pmod --directory /var/tmp # 저장소를 둘 장치에서 +python3 run.py --directory /var/tmp # 저장소를 둘 장치에서 ``` 저장 보장 쓰기는 장치 상태가 같을 때만 비교된다. SD 카드는 동기 쓰기가 이어지면 느려지므로 실행 순서만으로 @@ -19,8 +19,7 @@ python3 run.py --module editor.pmod --directory /var/tmp # 저장소를 둘 � 중앙값이다. ```sh -python3 run.py --module editor.pmod --workloads put --rounds 5 --seconds 3 \ - --only pure-serve,pure-serve-3,axum-durable,axum-atomic +python3 run.py --workloads put --rounds 5 --seconds 3 --only pure-program,axum-durable,axum-atomic ``` 앱 벤치마크([077](../../docs/research/077-apps-as-http-services.md))는 aup로 쓴 점수판(`scores.aup`)을 pure-serve가 보기와 diff --git a/benchmarks/http/run.py b/benchmarks/http/run.py index 2372595..38da9e1 100644 --- a/benchmarks/http/run.py +++ b/benchmarks/http/run.py @@ -132,7 +132,7 @@ def mixed(port, seconds, writers, readers): def main(): parser = argparse.ArgumentParser() - parser.add_argument('--module', type=Path, required=True, help='editor.pmod built by aup, or scores.pmod for --suite app') + parser.add_argument('--module', type=Path, help='scores.pmod built by aup, for --suite app') parser.add_argument('--suite', choices=['documents', 'app'], default='documents') parser.add_argument('--directory', type=Path, default=Path('/var/tmp')) parser.add_argument('--seconds', type=float, default=5) @@ -186,8 +186,14 @@ def main(): ('mixed, 8 score + 56 total conn', dict(connections=64, method='mixed', keys=64, mode='keep-alive', writes=True)), ] + if not args.module: parser.error('--suite app serves --module') for name in ('pure-program', 'axum-atomic', 'drogon-atomic'): del servers[name] # atomic is durable for a log already + else: + # The document contract (/objects, If-Match) is the earlier document server's alone since Pure 027's 4b: + # pure-serve has no keyed documents (a keyed list holds them, served through inputs and views). + for name in [name for name in servers if name.startswith('pure-serve')]: + del servers[name] if args.workloads: workloads = [w for w in workloads if re.search(args.workloads, w[0])] chosen = [name for name in servers if not args.only or name in args.only.split(',')] diff --git a/cli/edit-module.c b/cli/edit-module.c deleted file mode 100644 index 3ce272e..0000000 --- a/cli/edit-module.c +++ /dev/null @@ -1,179 +0,0 @@ -#define _POSIX_C_SOURCE 200809L -#include "module-store.h" -#include "store.h" -#include <inttypes.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -/* The document CLI contract of edit.c, driven by a verified Pure module - * instead of a document program. The module declares keyed messages named - * show and edit; edit has u32 fields start and remove and a bytes tail. */ -static bool number(const char *text, uint64_t limit, uint64_t *out) { - if (!*text) return false; - uint64_t value = 0; - for (; *text; ++text) { - if (*text < '0' || *text > '9') return false; - unsigned digit = (unsigned)(*text - '0'); - if (digit > limit || value > (limit - digit) / 10) return false; - value = value * 10 + digit; - } - *out = value; - return true; -} - -static const char *message(PureResult status) { - switch (status) { - case PURE_OK: return "ok"; - case PURE_NOT_FOUND: return "not_found"; - case PURE_CONFLICT: return "conflict"; - case PURE_BUSY: return "busy"; - case PURE_BUDGET: return "payload_budget"; - case PURE_LIMIT: return "limit"; - case PURE_UNCERTAIN: return "durability_unknown"; - case PURE_DENIED: return "denied"; - case PURE_ALLOCATION: return "allocation"; - case PURE_INVALID: return "invalid"; - default: return "failed"; - } -} - -static bool valid_key(const char *key) { - size_t length = strlen(key); - if (!length || length > 64) return false; - for (size_t i = 0; i < length; ++i) { - char c = key[i]; - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || - c == '-' || c == '_')) return false; - } - return true; -} - -static void put32(unsigned char *to, uint32_t value) { - for (unsigned i = 0; i < 4; ++i) to[i] = (unsigned char)(value >> (8 * i)); -} - -static int field(const PureModule *m, int input, const char *name, uint8_t type) { - const PureInputShape *shape = &m->input[input]; - for (uint32_t i = 0; i < shape->field_count; ++i) - if (!strcmp(m->field[shape->field_start + i].name, name) && m->field[shape->field_start + i].type == type) - return (int)(shape->field_start + i); - return -1; -} - -static bool write_span(void *context, PureSpan span) { - return fwrite(span.data, 1, span.size, context) == span.size; -} - -static bool count_span(void *context, PureSpan span) { - *(size_t *)context += span.size; - return true; -} - -int main(int argc, char **argv) { - const char *module_path = NULL, *store_path = NULL; - size_t budget = 4u * 1024u * 1024u; - int at = 1; - for (; at + 1 < argc && !strncmp(argv[at], "--", 2); at += 2) { - uint64_t value; - if (!strcmp(argv[at], "--store")) store_path = argv[at + 1]; - else if (!strcmp(argv[at], "--module")) module_path = argv[at + 1]; - else if (!strcmp(argv[at], "--budget") && number(argv[at + 1], SIZE_MAX, &value)) budget = (size_t)value; - else goto usage; - } - if (!module_path || !store_path || at >= argc) goto usage; - bool show = !strcmp(argv[at], "show"), edit = !strcmp(argv[at], "edit"); - if ((!show && !edit && strcmp(argv[at], "apply")) || - argc - at != (show ? 2 : edit ? 6 : 4)) goto usage; - uint64_t expected = 0, offset = 0, remove = 0; - if (!show && !number(argv[at + 2], INT64_MAX, &expected)) goto usage; - if (edit && (!number(argv[at + 3], UINT32_MAX, &offset) || !number(argv[at + 4], UINT32_MAX, &remove))) goto usage; - - static PureModule module; - char error[160]; - PureResult read = pure_module_read(module_path, &module, error, sizeof(error)); - if (read != PURE_OK) { - fprintf(stderr, "Module: %s\n", read == PURE_NOT_FOUND ? "cannot read module" : error); - return 2; - } - int input = pure_module_input(&module, show ? "show" : "edit"); - if (input < 0 || module.input[input].kind != PURE_KIND_MESSAGE || !module.input[input].keyed) { - fprintf(stderr, "Module: needs a keyed message named %s\n", show ? "show" : "edit"); - return 2; - } - const PureInputShape *shape = &module.input[input]; - int start_field = field(&module, input, "start", PURE_FIELD_U32); - int remove_field = field(&module, input, "remove", PURE_FIELD_U32); - size_t prefix = edit ? shape->prefix : 0; - if (edit && (start_field < 0 || remove_field < 0 || !shape->field_count || - module.field[shape->field_start + shape->field_count - 1].type != PURE_FIELD_BYTES)) { - fputs("edit requires u32 fields start and remove and trailing insertion bytes; use apply for a custom input\n", stderr); - return 2; - } - FILE *file = NULL; - size_t length = 0; - if (!show) { - file = fopen(argv[at + (edit ? 5 : 3)], "rb"); - if (!file) { perror("input"); return 2; } - long bytes; - if (fseek(file, 0, SEEK_END) || (bytes = ftell(file)) < 0 || - (uintmax_t)bytes > PURE_OBJECT_LIMIT - prefix || fseek(file, 0, SEEK_SET)) { - fputs("input must be a seekable file within the request size limit\n", stderr); - fclose(file); return 2; - } - length = (size_t)bytes; - } - if (pure_store_init(store_path) != PURE_OK) { - if (file) fclose(file); - fputs("cannot initialize store\n", stderr); return 2; - } - const char *key = argv[at + 1]; - if (!valid_key(key) || prefix + length > budget) { - if (file) fclose(file); - fprintf(stderr, "%s\n", valid_key(key) ? "payload_budget" : "invalid"); return 2; - } - unsigned char *payload = NULL; - if (!show) { - payload = calloc(1, prefix + length ? prefix + length : 1); - if (!payload) { fclose(file); fputs("allocation\n", stderr); return 2; } - if (edit) { - put32(payload + module.field[start_field].offset, (uint32_t)offset); - put32(payload + module.field[remove_field].offset, (uint32_t)remove); - } - bool valid = !length || fread(payload + prefix, 1, length, file) == length; - if (fgetc(file) != EOF || ferror(file)) valid = false; - if (fclose(file)) valid = false; - if (!valid) { - free(payload); - fputs("input changed or could not be read; nothing was applied\n", stderr); return 2; - } - } - PureApp app; - if (pure_app_start(&app, &module, pure_tree_data(store_path)) != PURE_OK) { - free(payload); - fputs("allocation\n", stderr); return 2; - } - PureResult status = pure_app_message(&app, (uint32_t)input, key, show ? -1 : (int64_t)expected, - (PureSpan){payload, prefix + length}); - size_t reply = 0; - if (status == PURE_OK) status = pure_app_reply(&app, count_span, &reply); - if (status == PURE_OK && show && reply > budget) { status = PURE_BUDGET; app.revision = 0; } - int exit_code = 0; - if (status != PURE_OK) { - fprintf(stderr, "{\"error\":\"%s\",\"revision\":\"%" PRId64 "\"}\n", message(status), app.revision); - exit_code = status == PURE_CONFLICT ? 3 : 2; - } else if (show) { - if (pure_app_reply(&app, write_span, stdout) != PURE_OK) exit_code = 2; - fprintf(stderr, "{\"revision\":\"%" PRId64 "\",\"bytes\":%zu}\n", app.revision, reply); - } else if (printf("{\"revision\":\"%" PRId64 "\",\"reply_bytes\":%zu}\n", app.revision, reply) < 0) exit_code = 2; - if (fflush(stdout)) exit_code = 2; - pure_app_stop(&app); - free(payload); - return exit_code; -usage: - fputs("pure-edit-module --module FILE --store DIRECTORY [--budget BYTES] show KEY\n" - "pure-edit-module --module FILE --store DIRECTORY [--budget BYTES] edit KEY REVISION BYTE_OFFSET REMOVE_BYTES INSERT_FILE\n" - "pure-edit-module --module FILE --store DIRECTORY [--budget BYTES] apply KEY REVISION INPUT_FILE\n" - "edit uses the module's start and remove fields; apply accepts its raw message bytes.\n", stderr); - return 2; -} diff --git a/cli/pure-run.c b/cli/pure-run.c index 8783f84..d3b7d1a 100644 --- a/cli/pure-run.c +++ b/cli/pure-run.c @@ -226,7 +226,7 @@ int main(int argc, char **argv) { if (failure) { } else if (input < 0) failure = "unknown input"; else if ((command[0] == 't' ? pure_app_text(&app, (uint32_t)input, given) - : pure_app_message(&app, (uint32_t)input, NULL, -1, given)) != PURE_OK) + : pure_app_message(&app, (uint32_t)input, given)) != PURE_OK) failure = "event failed"; free(read); } else if (!strcmp(command, "tick")) { diff --git a/core/module-save.c b/core/module-save.c index f87cd24..c70a181 100644 --- a/core/module-save.c +++ b/core/module-save.c @@ -15,8 +15,8 @@ * then its items (module-list.h). The log has an entry for each save that * changed lists: per list changed, {u8 name length, the name, u32le length, * the records of its changes (module-list.h) in its image's fields}. A save - * with such an entry that also commits the tree (numbers, texts, links, - * documents) stores its number there as the state "seq": the entry counts + * with such an entry that also commits the tree (numbers, texts, links) + * stores its number there as the state "seq": the entry counts * only when the tree holds its number (or a later one), so both are kept or * neither. A start * reads each list's image and makes again the changes logged after the save @@ -1059,15 +1059,8 @@ static unsigned char *list_changes(const PureApp *app, size_t *size) { return payload; } -PureResult pure_save_commit(PureApp *app, const PureValue *values, void *const *documents, size_t document_count, - int64_t *revisions) { - if (!pure_save_enabled(app)) { - /* Without a host that saves together, a save has one document at most. */ - PureResult result = PURE_OK; - for (size_t i = 0; i < document_count && result == PURE_OK; ++i) - result = app->data.commit(app->data.context, documents[i], &revisions[i]); - return result; - } +PureResult pure_save_commit(PureApp *app, const PureValue *values) { + if (!pure_save_enabled(app)) return PURE_OK; /* the states live in memory alone */ /* A save that may or may not have been stored: until the app starts again * from what the store has, no save follows it. */ if (app->uncertain) return PURE_CONFLICT; @@ -1125,7 +1118,7 @@ PureResult pure_save_commit(PureApp *app, const PureValue *values, void *const * size_t payload_size = 0; unsigned char *payload = result == PURE_OK ? list_changes(app, &payload_size) : NULL; if (payload_size && !payload) result = PURE_ALLOCATION; - bool tree = count || document_count; + bool tree = count; uint64_t seq = app->seq + 1; if (result == PURE_OK && tree && payload_size && app->log) { result = app->stored_seq ? PURE_OK : open_state(app, "", "seq", &app->stored_seq); @@ -1146,7 +1139,7 @@ PureResult pure_save_commit(PureApp *app, const PureValue *values, void *const * } free(payload); if (result == PURE_OK && tree) { - result = app->data.save(app->data.context, documents, document_count, drafts, count, revisions); + result = app->data.save(app->data.context, drafts, count); /* A commit that may yet be stored leaves the app apart from its store, * whatever the save changed: no save follows until it starts again. * One that failed takes back the logged entry, which is on the device. */ diff --git a/core/module-save.h b/core/module-save.h index 281854b..83ffe60 100644 --- a/core/module-save.h +++ b/core/module-save.h @@ -54,10 +54,8 @@ PureResult pure_save_open_values(PureApp *, const PureModule *m, PureValue *valu * runtime lists kept the numbers heard and the sends: they go to them * (moved: they need images). */ PureResult pure_save_open_links(PureApp *, const PureModule *m, bool *heard_moved, bool *sent_moved); -/* Publish the documents and every state changed since the last save - * (values are the states' numbers); revisions are the documents'. */ -PureResult pure_save_commit(PureApp *, const PureValue *values, void *const *documents, size_t document_count, - int64_t *revisions); +/* Publish every state changed since the last save (values are the states' numbers). */ +PureResult pure_save_commit(PureApp *, const PureValue *values); /* A message's fields laid out as load lays them: in order, the fixed ones * and the variable ones' lengths in the prefix, but the last variable diff --git a/core/module-store.c b/core/module-store.c index 92200af..7ca2dc0 100644 --- a/core/module-store.c +++ b/core/module-store.c @@ -1,92 +1,31 @@ #include "module-store.h" -static PureResult open_document(void *context, uint32_t collection, const char *key, - void **document, int64_t *revision) { - (void)collection; /* v2 modules have one collection: the store's key space */ - PureDraft *draft; - PureResult result = pure_draft_open(context, key, &draft); - if (result != PURE_OK) return result; - pure_draft_hold(draft, true); /* values read in the event stay readable */ - *document = draft; - *revision = pure_draft_revision(draft); - return PURE_OK; -} - -static PureVersion version(void *context, void *document) { - (void)context; - return pure_draft_version(document); -} - -static PureResult read(void *context, void *document, PureVersion at, size_t offset, size_t length, - bool (*visit)(void *, PureSpan), void *visit_context) { - (void)context; - return pure_draft_read_version(document, at, offset, length, visit, visit_context); -} - -static PureResult splice(void *context, void *document, size_t offset, size_t remove, PureSpan insert) { - (void)context; - return pure_draft_splice(document, offset, remove, insert); -} - -static PureResult commit(void *context, void *document, int64_t *revision) { - (void)context; - return pure_draft_commit(document, revision); -} - -static void close_document(void *context, void *document) { - (void)context; - pure_draft_close(document); -} - static PureResult state(void *context, const char *name, PureDraft **out) { return pure_state_open(context, name, out); } -enum { SAVED = PURE_APP_PENDING + 2 * PURE_MODULE_STATES + 1 }; +enum { SAVED = 2 * PURE_MODULE_STATES + 3 }; -/* The documents and the app's changed states in one tree commit. */ -static PureResult save(void *context, void *const *documents, size_t document_count, PureDraft *const *states, - size_t count, int64_t *revisions) { +/* The app's changed states in one tree commit. */ +static PureResult save(void *context, PureDraft *const *states, size_t count) { (void)context; - PureDraft *drafts[SAVED] = {0}; - int64_t all[SAVED] = {0}; - if (document_count > PURE_APP_PENDING || count > SAVED - document_count) return PURE_LIMIT; - for (size_t i = 0; i < document_count; ++i) drafts[i] = documents[i]; - for (size_t i = 0; i < count; ++i) drafts[document_count + i] = states[i]; - PureResult result = pure_draft_commit_all(drafts, document_count + count, all); - for (size_t i = 0; i < document_count; ++i) revisions[i] = all[i]; - return result; + int64_t revisions[SAVED]; + if (count > SAVED) return PURE_LIMIT; + return pure_draft_commit_all(states, count, revisions); } static const char *path_directory(void *context) { return context; } static const char *handle_directory(void *context) { return pure_store_path(context); } -/* Through a store handle: documents and states share its mapping. */ -static PureResult open_handle_document(void *context, uint32_t collection, const char *key, void **document, - int64_t *revision) { - (void)collection; - PureDraft *draft; - PureResult result = pure_store_draft(context, key, &draft); - if (result != PURE_OK) return result; - pure_draft_hold(draft, true); - *document = draft; - *revision = pure_draft_revision(draft); - return PURE_OK; -} - static PureResult handle_state(void *context, const char *name, PureDraft **out) { return pure_store_state(context, name, out); } PureData pure_store_data(PureStore *store) { - return (PureData){.context = store, .open = open_handle_document, .version = version, .read = read, - .splice = splice, .commit = commit, .close = close_document, .state = handle_state, - .save = save, .directory = handle_directory}; + return (PureData){.context = store, .state = handle_state, .save = save, .directory = handle_directory}; } 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, - .state = state, .save = save, .directory = path_directory}; + return (PureData){.context = (void *)path, .state = state, .save = save, .directory = path_directory}; } diff --git a/core/module-store.h b/core/module-store.h index d14ab3d..fd5bc6b 100644 --- a/core/module-store.h +++ b/core/module-store.h @@ -1,11 +1,9 @@ #ifndef PURE_MODULE_STORE_H #define PURE_MODULE_STORE_H #include "module.h" -/* 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. */ +/* The app's states on the selected tree store, each save one commit. The path outlives the app. */ PureData pure_tree_data(const char *path); -/* The same through a store handle (state.h): opening a document or state - * checks only whether the store changed. The handle outlives the app. */ +/* The same through a store handle (state.h): opening a state checks only whether the store changed. The handle outlives + * the app. */ PureData pure_store_data(PureStore *); #endif diff --git a/core/module.c b/core/module.c index b8782f3..5ebecea 100644 --- a/core/module.c +++ b/core/module.c @@ -10,7 +10,7 @@ #include <string.h> enum { FUNCTION_BYTES = 12, SIGNATURE_BYTES = 4 }; -enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 8, +enum { STATE_BYTES = 16, INPUT_BYTES = 16, FIELD_BYTES = 8, CONSTANT_BYTES = 8, RECORD_BYTES = 8, RECORD_FIELD_BYTES = 16, VIEW_BYTES = 16, HANDLER_BYTES = 12, OP_BYTES = 16 }; /* T_FIELD and T_ITEM stand for the type of the message or record field @@ -19,25 +19,25 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = * state named. */ enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8, T_LOCAL = 9, T_BOOL = 10, T_STATE = 11, T_I64 = 12, T_LIST = 13 }; /* T_LIST: a list value, its owner the record of its items */ -enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS, FROM_HELD }; +enum { FROM_PAYLOAD, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS, FROM_HELD }; /* Where an opcode may appear. */ /* IN_ARGUMENTS: fields read where nothing is done with them (a view's arguments, a message's fields in a fold). */ /* IN_LOCALS: a handler's locals, read in its walks and the folds in them. */ /* IN_COMPUTE: what a view's body may do too (018): loops, locals, and changes to an event's lists alone. */ /* IN_FUNCTION: a function's code (027's 2c), a handler's but for a message. */ -enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8, IN_TEXT = 16, IN_ROW = 32, IN_OUTER = 64, IN_ARGUMENTS = 128, +enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_TEXT = 16, IN_ROW = 32, IN_OUTER = 64, IN_ARGUMENTS = 128, IN_LOCALS = 256, IN_COMPUTE = 512, IN_FUNCTION = 1024 }; enum { READ = IN_VIEW | IN_HANDLER }; /* What an argument names: nothing (it is 0), a state of one type (A_STATE + - * the type), a list and one of its record's fields, a message field, the - * collection, a constant, a unit, the column's row, a branch target, a stack + * the type), a list and one of its record's fields, a message field, a + * constant, a unit, the column's row, a branch target, a stack * depth, a fold, a relation, a keyed list, an output, the row of the column * a fold runs in, a number, bool or i64 state (A_VALUE), or the value's type * (A_TYPE: 0 a number, 1 a bool, 2 an i64), how I64_BITS computes (A_BITS), * what a position is (A_POSITION: 0 a number, 1 an i64), what CONCAT joins * (A_JOINED: 0 texts, 1 bytes), how NUM_TEXT writes (A_FORMAT) or the value * held by name (A_HELD), or a function (A_FUNCTION: CALL's, and RETURN's own). */ -enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_I64 + 1, A_FIELD, A_COLLECTION, A_CONSTANT, A_UNIT, +enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_I64 + 1, A_FIELD, A_CONSTANT, A_UNIT, A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT, A_OUTER, A_EACH, A_LOCAL, A_VALUE, A_TYPE, A_LOOP, A_BITS, A_POSITION, A_JOINED, A_FORMAT, A_HELD, A_FUNCTION }; /* A fold's block is verified as a column that leaves one bool (count, first) @@ -56,7 +56,7 @@ enum { FOLD_ONE = 100, FOLD_TWO, FOLD_I64, FILTER, WALK, STEPS, STEPS_WALK, FUNC * O_REF: the text or list of the argument's cursor or index state. O_LIST: * the list of an item argument. */ enum { O_ANY, O_OPERAND, O_ARG, O_REF, O_LIST }; -static const unsigned char magic[8] = {'P','U','R','E','M','O','D','6'}; +static const unsigned char magic[8] = {'P','U','R','E','M','O','D','7'}; /* Operand types popped (bottom to top), the type pushed, where allowed, what * the argument names and the text or list worked on. */ @@ -77,10 +77,7 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi [PURE_OP_JZ] = {1, {T_BOOL}, 0, READ, A_TARGET, O_ANY}, [PURE_OP_JMP] = {0, {0}, 0, READ, A_TARGET, O_ANY}, [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE | IN_ARGUMENTS, A_FIELD, O_ANY}, - [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED, A_COLLECTION, O_ANY}, - [PURE_OP_DOC_SIZE] = {0, {0}, T_NUM, IN_KEYED, A_COLLECTION, O_ANY}, [PURE_OP_LEN] = {1, {T_BYTES}, T_NUM, READ, A_NONE, O_ANY}, - [PURE_OP_REPLACE] = {3, {T_NUM, T_NUM, T_BYTES}, 0, IN_KEYED, A_COLLECTION, O_ANY}, [PURE_OP_REPLY] = {1, {T_BYTES}, 0, IN_MESSAGE, A_NONE, O_ANY}, [PURE_OP_REQUIRE] = {1, {T_BOOL}, 0, IN_HANDLER, A_NONE, O_ANY}, [PURE_OP_TEXT_CONST] = {0, {0}, T_TEXT, READ, A_CONSTANT, O_ANY}, @@ -309,18 +306,6 @@ PURE_COLD static bool key_name(const char *key) { return false; } -/* A document key: 1..64 bytes of [A-Za-z0-9_-], as every Pure store accepts. */ -static bool document_key(const char *key) { - size_t length = key ? strlen(key) : 0; - if (!length || length > 64) return false; - for (size_t i = 0; i < length; ++i) { - char c = key[i]; - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || - c == '-' || c == '_')) return false; - } - return true; -} - /* Strings are [u8 length][bytes]; a name is 1..31 bytes: an ASCII identifier, * a path of identifiers joined by dots (a message field inside a group, as * user.name), or any printable ASCII (a key's name). */ @@ -438,7 +423,6 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg case A_ITEM: return state_of(m, list_of(arg), PURE_STATE_LIST) && item_field(arg) < m->record[m->state_ref[list_of(arg)]].count; case A_FIELD: return arg < shape->field_count && m->field[shape->field_start + arg].type != PURE_FIELD_ROWS; - case A_COLLECTION: return arg < m->collections; case A_CONSTANT: return arg < m->constants; case A_UNIT: return arg <= PURE_UNIT_LINE; case A_ROW: return (int)arg == row; @@ -519,7 +503,7 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i unsigned where = steps ? IN_VIEW | IN_COMPUTE | (view == STEPS_WALK ? IN_ROW : 0) : !handler ? IN_VIEW | (row >= 0 ? IN_ROW : 0) | (folding && outer >= 0 ? IN_OUTER : 0) | (body ? IN_COMPUTE : 0) : IN_HANDLER | (view == WALK ? IN_ROW : 0); - if (shape && handler && !steps && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0); + if (shape && handler && !steps && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE; if (shape && handler && !steps && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT; if (shape && (!handler || steps) && (shape->kind == PURE_KIND_MESSAGE || shape->kind == PURE_KIND_ARGUMENTS)) where |= IN_ARGUMENTS; @@ -568,7 +552,6 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i if (view == FILTER) REFUSE("filter with an effect"); if (folding) REFUSE("fold with an effect"); if (!handler || steps) REFUSE("view with an effect"); - if (allowed & IN_KEYED) REFUSE("document operation outside a keyed message handler"); if (allowed & IN_TEXT) REFUSE("text input outside a text handler"); REFUSE("message operation outside a message handler"); } @@ -1602,7 +1585,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule if (error_size) error[0] = 0; pure_module_release(out); memset(out, 0, sizeof(*out)); - if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic))) REFUSE("not a Pure module v6", 0); + if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic))) REFUSE("not a Pure module v7", 0); out->states = (uint16_t)read16(p + 8); out->inputs = (uint16_t)read16(p + 10); out->views = (uint16_t)read16(p + 12); @@ -1610,24 +1593,21 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule out->code_count = read32(p + 16); uint32_t strings = read32(p + 20); out->fields = (uint16_t)read16(p + 24); - out->collections = (uint16_t)read16(p + 26); - out->constants = (uint16_t)read16(p + 28); - out->records = (uint16_t)read16(p + 30); - uint32_t constant_bytes = read32(p + 32); - out->record_fields = (uint16_t)read16(p + 36); - out->functions = (uint16_t)read16(p + 38); + out->constants = (uint16_t)read16(p + 26); + out->records = (uint16_t)read16(p + 28); + uint32_t constant_bytes = read32(p + 30); + out->record_fields = (uint16_t)read16(p + 34); + out->functions = (uint16_t)read16(p + 36); if (out->states > PURE_MODULE_STATES || out->inputs > PURE_MODULE_INPUTS || out->views > PURE_MODULE_VIEWS || out->handlers > PURE_MODULE_HANDLERS || out->code_count > PURE_MODULE_CODE || strings > PURE_MODULE_STRINGS || - out->fields > PURE_MODULE_FIELDS || out->collections > PURE_MODULE_COLLECTIONS || - out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES || + out->fields > PURE_MODULE_FIELDS || out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES || out->records > PURE_MODULE_RECORDS || out->record_fields > PURE_MODULE_RECORD_FIELDS || out->functions > PURE_MODULE_FUNCTIONS) { result = PURE_LIMIT; REFUSE("module exceeds a limit", 8); } - size_t functions_at = PURE_MODULE_HEADER + (size_t)out->states * STATE_BYTES + - (size_t)out->collections * COLLECTION_BYTES + (size_t)out->inputs * INPUT_BYTES + + size_t functions_at = PURE_MODULE_HEADER + (size_t)out->states * STATE_BYTES + (size_t)out->inputs * INPUT_BYTES + (size_t)out->fields * FIELD_BYTES + (size_t)out->constants * CONSTANT_BYTES + (size_t)out->records * RECORD_BYTES + (size_t)out->record_fields * RECORD_FIELD_BYTES + (size_t)out->views * VIEW_BYTES + (size_t)out->handlers * HANDLER_BYTES; @@ -1695,24 +1675,19 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule out->state_key[i] = (uint8_t)key; } } - for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) { - if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->collection_names[i])) REFUSE("collection name", i); - if (read32(at + 4) != 1 || read32(at + 8) != 1) REFUSE("collection must hold bytes by key", i); - } uint32_t next_field = 0; const unsigned char *fields = at + (size_t)out->inputs * INPUT_BYTES; for (uint32_t i = 0; i < out->inputs; ++i, at += INPUT_BYTES) { PureInputShape *shape = &out->input[i]; shape->kind = at[4]; - shape->keyed = at[5]; shape->field_start = (uint16_t)read16(at + 12); shape->field_count = (uint16_t)read16(at + 14); /* Arguments bear their view's name (view.args); other inputs are identifiers. */ if (!name_at(text, strings, read32(at), shape->kind == PURE_KIND_ARGUMENTS ? NAME_PATH : NAME_IDENTIFIER, out->input_names[i])) REFUSE("input name", i); - if (read16(at + 6) || at[5] > 1) REFUSE("input flags", i); + if (read16(at + 6) || at[5]) REFUSE("input flags", i); if (shape->kind == PURE_KIND_KEY) { - if (shape->keyed || shape->field_start || shape->field_count || + if (shape->field_start || shape->field_count || !name_at(text, strings, read32(at + 8), NAME_PRINTABLE, out->input_keys[i]) || !key_name(out->input_keys[i])) REFUSE("input key", i); for (uint32_t k = 0; k < i; ++k) @@ -1721,19 +1696,18 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule continue; } if (shape->kind == PURE_KIND_TEXT) { - if (shape->keyed || shape->field_start || shape->field_count || read32(at + 8)) - REFUSE("text input shape", i); + if (shape->field_start || shape->field_count || read32(at + 8)) REFUSE("text input shape", i); continue; } if (shape->kind == PURE_KIND_TIMER) { shape->period = read32(at + 8); - if (shape->keyed || shape->field_start || shape->field_count || !shape->period || + if (shape->field_start || shape->field_count || !shape->period || shape->period > PURE_TIMER_LONGEST) REFUSE("timer input", i); continue; } bool arguments = shape->kind == PURE_KIND_ARGUMENTS; if ((shape->kind != PURE_KIND_MESSAGE && shape->kind != PURE_KIND_OUTPUT && !arguments) || (!arguments && read32(at + 8)) || - ((shape->kind == PURE_KIND_OUTPUT || arguments) && shape->keyed) || shape->field_start != next_field || + shape->field_start != next_field || shape->field_count > out->fields - next_field) REFUSE("message shape", i); shape->view = arguments ? read32(at + 8) : 0; for (uint32_t f = 0; f < shape->field_count; ++f) { @@ -1892,13 +1866,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule if (at[1] || at[2] || at[3]) REFUSE("instruction padding", i); out->code[i] = (PureOp){.op = at[0], .arg = read32(at + 4), .value = read_double(at + 8)}; } - /* States, records, collections, inputs and views share one namespace. */ - const char *names[PURE_MODULE_STATES + PURE_MODULE_RECORDS + PURE_MODULE_COLLECTIONS + - PURE_MODULE_INPUTS + PURE_MODULE_VIEWS]; + /* States, records, inputs and views share one namespace. */ + const char *names[PURE_MODULE_STATES + PURE_MODULE_RECORDS + PURE_MODULE_INPUTS + PURE_MODULE_VIEWS]; size_t count = 0; for (uint32_t i = 0; i < out->states; ++i) names[count++] = out->state_names[i]; for (uint32_t i = 0; i < out->records; ++i) names[count++] = out->record_names[i]; - for (uint32_t i = 0; i < out->collections; ++i) names[count++] = out->collection_names[i]; for (uint32_t i = 0; i < out->inputs; ++i) names[count++] = out->input_names[i]; for (uint32_t i = 0; i < out->views; ++i) if (!out->view_table[i]) names[count++] = out->view_names[i]; for (size_t i = 0; i < count; ++i) @@ -1954,6 +1926,20 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule if ((why = verify_block(out, out->handler[i].code, -1, -1, -1, out->handler[i].event, true, 0, 0, -1, &bad))) goto refused; if (!locals_agree(out, out->handler[i].code)) { why = "local used as two things"; goto refused; } } + /* An instruction in no handler's, view's or function's block never runs and was never verified: it becomes CONST 0, + * so the passes below, which read every instruction's arguments, read only what was verified. */ + unsigned char *held = calloc(out->code_count + 1, 1); + if (!held) { + result = PURE_ALLOCATION; + REFUSE("out of memory", 0); + } + for (uint32_t i = 0; i < out->handlers; ++i) memset(held + out->handler[i].code.start, 1, out->handler[i].code.length); + for (uint32_t i = 0; i < out->functions; ++i) memset(held + out->function[i].code.start, 1, out->function[i].code.length); + for (uint32_t i = 0; i < out->views; ++i) + if (out->view_code[i].length) memset(held + out->view_code[i].start, 1, out->view_code[i].length); + for (uint32_t pc = 0; pc < out->code_count; ++pc) + if (!held[pc]) out->code[pc] = (PureOp){.op = PURE_OP_CONST}; + free(held); for (uint32_t i = 0; i < out->views; ++i) { int given = (int)out->view_arguments[i] - 1; plan_folds(out, out->view_code[i], given); @@ -2264,12 +2250,9 @@ static PureResult visit_value(const PureApp *app, const PureArena *arena, PureBy case FROM_CONSTANT: direct = app->module->constant_data + v.offset; break; case FROM_ARENA: direct = arena->bytes + v.offset; break; case FROM_HELD: direct = app->held[v.owner].data + v.offset; break; - case FROM_TEXT: + default: /* FROM_TEXT */ return pure_draft_read_version(app->texts[v.owner], v.version, (size_t)v.offset, (size_t)v.length, visit, context); - default: - return app->data.read(app->data.context, app->document, v.version, (size_t)v.offset, - (size_t)v.length, visit, context); } visit(context, (PureSpan){direct, (size_t)v.length}); return PURE_OK; @@ -3719,31 +3702,7 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp ev->spans[arg][1]}; NEXT; } - case PURE_OP_DOC: case PURE_OP_DOC_SIZE: { - if (op->op == PURE_OP_DOC && !ev->exists) { kind = PURE_FAULT_NOT_FOUND; FAILED; } - PureVersion version = app->data.version(app->data.context, app->document); - if (op->op == PURE_OP_DOC) stack[sp++].bytes = (PureBytes){FROM_DOCUMENT, 0, version, 0, version.bytes}; - else stack[sp++].num = (double)version.bytes; - NEXT; - } case PURE_OP_LEN: case PURE_OP_TEXT_BYTES: top->num = (double)top->bytes.length; NEXT; - case PURE_OP_REPLACE: { - PureBytes insert = stack[--sp].bytes; - double last = stack[--sp].num, first = stack[--sp].num; - double size = (double)app->data.version(app->data.context, app->document).bytes; - if (first != floor(first) || last != floor(last) || first < 0 || last < first || last > size) { - kind = PURE_FAULT_LIMIT; - FAILED; - } - PureSpan span; - size_t mark = arena->used; - if ((status = contiguous(app, arena, insert, &span)) == PURE_OK) - status = app->data.splice(app->data.context, app->document, (size_t)first, (size_t)(last - first), span); - arena->used = mark; - if (status != PURE_OK) REFUSED; - ev->modified = ev->exists = true; - NEXT; - } /* A reply outlives the arena mark of later statements: keep it by value. */ case PURE_OP_REPLY: { /* by value; a value held by name may change after it, so its bytes go to the arena */ PureBytes reply = stack[--sp].bytes; @@ -4773,25 +4732,8 @@ PURE_COLD static PureList *new_list(const PureModule *m, uint32_t record, bool k return pure_list_new(r->count, kinds); } -/* Where key is in the pending index: its slot, or the empty one it would take. */ -static unsigned pending_slot(const PureApp *app, const char *key) { - uint32_t hash = 2166136261u; /* FNV-1a */ - for (const char *c = key; *c; ++c) hash = (hash ^ (unsigned char)*c) * 16777619u; - unsigned mask = 2 * PURE_APP_PENDING - 1, slot = hash & mask; - while (app->pending_index[slot] && strcmp(app->pending[app->pending_index[slot] - 1].key, key)) slot = (slot + 1) & mask; - return slot; -} - -bool pure_app_pending(const PureApp *app, const char *key) { - return app->pending_count && app->pending_index[pending_slot(app, key)]; -} - void pure_app_finish(PureApp *app) { - /* A document in the unsaved burst stays open until the save. */ - if (app->document && !app->document_pending) app->data.close(app->data.context, app->document); - app->document_pending = false; free(app->copied); - app->document = NULL; app->copied = NULL; app->has_reply = false; app->payload = (PureSpan){0}; @@ -4820,14 +4762,8 @@ static void open_burst(PureApp *app) { app->open = true; } -/* End a burst, keeping it (saved) or undoing all of it. The documents it held - * close, except the current event's, which pure_app_finish closes. */ +/* End a burst, keeping it (saved) or undoing all of it. */ static void close_burst(PureApp *app, bool keep) { - for (unsigned i = 0; i < app->pending_count; ++i) - if (app->pending[i].document != app->document) app->data.close(app->data.context, app->pending[i].document); - if (app->pending_count) memset(app->pending_index, 0, sizeof(app->pending_index)); - app->pending_count = 0; - app->document_pending = false; if (!app->open) return; const PureModule *m = app->module; for (uint32_t i = 0; i < m->states; ++i) { @@ -4866,12 +4802,7 @@ static void settle_links(PureApp *app, bool saved) { } PureResult pure_app_save(PureApp *app) { - void *documents[PURE_APP_PENDING]; - int64_t revisions[PURE_APP_PENDING]; - size_t count = 0; - for (unsigned i = 0; i < app->pending_count; ++i) - if (app->pending[i].edited) documents[count++] = app->pending[i].document; - PureResult result = pure_save_commit(app, app->values, documents, count, revisions); + PureResult result = pure_save_commit(app, app->values); settle_links(app, result == PURE_OK); close_burst(app, result == PURE_OK); /* Images due are written now, unless the host writes them on another thread. */ @@ -4988,7 +4919,6 @@ void pure_app_stop(PureApp *app) { pure_draft_close(app->stored_seq); pure_save_close_start(app); pure_log_close(app->log); - free(app->pending); pure_undo_free(&app->undo); for (unsigned i = 0; i < app->row_tables; ++i) free(app->rows[i].items); free(app->rows); @@ -5105,8 +5035,7 @@ size_t pure_app_state_bytes(const PureApp *app) { /* One event: every handler for it runs in module order against pending * states, all or nothing. A key, text or message event saves before it * returns unless saving is deferred; a frame leaves that to the next event - * (frames come 60 times a second). A message's document joins the burst. - * With a memory budget, an event that leaves the states over it, and + * (frames come 60 times a second). With a memory budget, an event that leaves the states over it, and * holding more than before, fails. */ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { const PureModule *m = app->module; @@ -5131,13 +5060,8 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { if (m->handler[i].event == event) result = charge(app, m->handler[i].code.length) ? run(m, pending, m->handler[i].code, ev, NULL) : fault(app, PURE_FAULT_WORK, m->handler[i].code.start); - /* An edit needs the revision it was made against. A save rebases an - * edited document and releasing texts may reclaim earlier text versions: - * keep such a reply by copying it first. */ - if (result == PURE_OK && ev->modified && ev->base < 0) result = PURE_INVALID; - if (result == PURE_OK && app->has_reply && - ((app->reply.source == FROM_DOCUMENT && ev->modified) || app->reply.source == FROM_TEXT)) - result = keep_reply(app); + /* Releasing texts may reclaim earlier text versions: keep a text's reply by copying it first. */ + if (result == PURE_OK && app->has_reply && app->reply.source == FROM_TEXT) result = keep_reply(app); /* A delivery's number is heard with its changes, and an answer's sends go with it. */ if (result == PURE_OK && app->delivering.pending) result = hear(app, app->delivering.source, app->delivering.seq); if (result == PURE_OK && app->delivering.pending && app->delivering.release) @@ -5160,14 +5084,6 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { ++app->events; ++app->changes; if (!event) ++app->ticks; - if (app->document) { - app->pending_index[pending_slot(app, ev->key)] = (uint16_t)(app->pending_count + 1); /* not there: a key joins once */ - app->pending[app->pending_count].document = app->document; - app->pending[app->pending_count].edited = ev->modified; - memcpy(app->pending[app->pending_count++].key, ev->key, strlen(ev->key) + 1); - app->document_pending = true; - app->revision += ev->modified; /* a save moves an edited document one revision */ - } app->delivering.pending = false; return event && !app->deferred ? pure_app_save(app) : PURE_OK; } @@ -5279,38 +5195,16 @@ PureResult pure_app_arguments(PureApp *app, uint32_t view, PureSpan payload, con /* A message whose variable fields are where placed says (start and size in * the payload), or, without it, one after another after the prefix. */ -static PureResult message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload, - const uint32_t *placed) { +static PureResult message(PureApp *app, uint32_t input, PureSpan payload, const uint32_t *placed) { pure_app_finish(app); const PureModule *m = app->module; app->fault = PURE_FAULT_NONE; app->fault_pc = UINT32_MAX; - app->revision = 0; - if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || base < -1 || - (payload.size && !payload.data) || (m->input[input].keyed ? !document_key(key) : key != NULL)) - return PURE_INVALID; + if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || (payload.size && !payload.data)) return PURE_INVALID; const PureInputShape *shape = &m->input[input]; uint32_t spans[PURE_MODULE_FIELDS][2]; PureFault wrong = place_fields(m, shape, payload, placed, spans); if (wrong != PURE_FAULT_NONE) return fault(app, wrong, UINT32_MAX); - if (shape->keyed && m->collections) { - if (!app->data.open) return PURE_INVALID; - /* A key joins a burst once, so its revision moves once per save. */ - if (app->pending_count == PURE_APP_PENDING || pure_app_pending(app, key)) { - PureResult saved = pure_app_save(app); - if (saved != PURE_OK) return saved; - } - if (app->pending_count == app->pending_capacity) { - unsigned capacity = app->pending_capacity ? 2 * app->pending_capacity : 16; - struct PurePending *grown = realloc(app->pending, capacity * sizeof(*grown)); - if (!grown) return PURE_ALLOCATION; - app->pending = grown; - app->pending_capacity = capacity; - } - PureResult opened = app->data.open(app->data.context, 0, key, &app->document, &app->revision); - if (opened != PURE_OK) { app->document = NULL; return opened; } - if (base >= 0 && base != app->revision) { pure_app_finish(app); return PURE_CONFLICT; } - } /* Its rows fill their lists (emptied when the event ends); rows that are not whole fail it. Rows whose records hold * lists (027's 4a) read those into the event's lists as a kept list's cell is read, in the event's lists begun anew. */ bool nested = false; @@ -5335,20 +5229,16 @@ static PureResult message(PureApp *app, uint32_t input, const char *key, int64_t return filled == PURE_ALLOCATION ? filled : fault(app, PURE_FAULT_LIMIT, UINT32_MAX); } app->payload = payload; - Event ev = {.app = app, .shape = shape, .key = key, .base = base, .arena = &app->arena, .exists = app->revision > 0, - .nested = nested, .spans = spans}; + Event ev = {.app = app, .shape = shape, .arena = &app->arena, .nested = nested, .spans = spans}; PureResult result = dispatch(app, 1 + 2 * input, &ev); if (result != PURE_OK) pure_app_finish(app); return result; } -PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload) { - return message(app, input, key, base, payload, NULL); -} +PureResult pure_app_message(PureApp *app, uint32_t input, PureSpan payload) { return message(app, input, payload, NULL); } -PureResult pure_app_message_fields(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload, - const uint32_t *placed) { - return message(app, input, key, base, payload, placed); +PureResult pure_app_message_fields(PureApp *app, uint32_t input, PureSpan payload, const uint32_t *placed) { + return message(app, input, payload, placed); } /* The outbox's item at: its output (the module's, by name), number and payload. */ @@ -5388,7 +5278,7 @@ static PureResult deliver(PureApp *app, uint32_t input, uint32_t release, const size_t length = source ? strlen(source) : 0; app->duplicate = false; /* Kept states without a directory would not keep the numbers heard. */ - if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || m->input[input].keyed || !length || + if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || !length || length > PURE_SOURCE || !seq || seq > PURE_SEQ_MOST || !app->heard || (pure_save_enabled(app) && !app->log)) return PURE_INVALID; for (size_t i = 0; i < length; ++i) @@ -5402,7 +5292,7 @@ static PureResult deliver(PureApp *app, uint32_t input, uint32_t release, const app->delivering.seq = seq; app->delivering.release = release; app->delivering.pending = true; - PureResult result = message(app, input, NULL, -1, payload, placed); + PureResult result = message(app, input, payload, placed); if (!app->delivering.pending) return result; /* Not applied. Refused by the app (a fault), the message was delivered * all the same: its number is heard, saved like a key event's changes. @@ -6305,7 +6195,7 @@ PURE_COLD PureResult pure_app_replace(PureApp *app, const PureModule *module) { drop_nested(app); drop_spares(app); pure_app_finish(app); - if (app->open || app->pending_count) { + if (app->open) { PureResult saved = pure_app_save(app); /* the burst belongs to the module that made it */ if (saved != PURE_OK) return saved; } diff --git a/core/module.h b/core/module.h index d23cb49..d155b41 100644 --- a/core/module.h +++ b/core/module.h @@ -8,7 +8,7 @@ #include <stddef.h> #include <stdint.h> -/* Pure module format v6 (docs/design/008-module-format.md). A toolchain such +/* Pure module format v7 (docs/design/008-module-format.md). A toolchain such * as aup produces it; Pure verifies it completely before anything executes: * operand types (including which text a cursor and which list an index * belongs to), stack heights and the stack at every join, forward-only @@ -16,10 +16,10 @@ enum { PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 64, PURE_MODULE_HANDLERS = 64, PURE_MODULE_CODE = 65535, PURE_MODULE_STACK = 64, - PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 40, - PURE_MODULE_FIELDS = 64, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 4096, + PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 38, + PURE_MODULE_FIELDS = 64, PURE_MODULE_CONSTANTS = 4096, PURE_MODULE_CONSTANT_BYTES = 1 << 20, PURE_MODULE_RECORDS = 16, PURE_MODULE_RECORD_FIELDS = 128, - PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8, PURE_APP_PENDING = 256 + PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8 }; typedef enum { @@ -27,7 +27,7 @@ typedef enum { PURE_OP_DIV, PURE_OP_NEG, PURE_OP_LT, PURE_OP_LE, PURE_OP_GT, PURE_OP_GE, PURE_OP_EQ, PURE_OP_NE, PURE_OP_AND, PURE_OP_OR, PURE_OP_NOT, PURE_OP_MIN, PURE_OP_MAX, PURE_OP_CLAMP, PURE_OP_STORE, PURE_OP_JZ, PURE_OP_JMP, - PURE_OP_FIELD, PURE_OP_DOC, PURE_OP_DOC_SIZE, PURE_OP_LEN, PURE_OP_REPLACE, + PURE_OP_FIELD, PURE_OP_LEN, PURE_OP_REPLY, PURE_OP_REQUIRE, PURE_OP_TEXT_CONST, PURE_OP_TEXT_LOAD, PURE_OP_TEXT_INPUT, PURE_OP_NUM_TEXT, PURE_OP_CONCAT, PURE_OP_CHARS, PURE_OP_LINES, PURE_OP_TEXT_BYTES, @@ -126,8 +126,8 @@ enum { PURE_FORMAT_WIDTH = 8, PURE_FORMAT_ZEROS = 1 << 16, PURE_FORMAT_FORM = 17 * locals (0 as it begins), and its own of what the function's code names: * values held by name (empty), an event's lists (empty) and registers (bound * to none); the caller's are as they were when the call returns. A function - * runs as a handler's code does but for a message (no field, document or - * reply); a handler and the functions run CALL (not a view), and a walk around + * runs as a handler's code does but for a message (no field or reply); a + * handler and the functions run CALL (not a view), and a walk around * a call keeps to what the callee changes as it keeps to its own block's * changes. Calls nest as deep as the event's room allows: each counts its * frame's bytes there (MEMORY past it) and its block's length as work. @@ -308,7 +308,6 @@ typedef struct { typedef struct { char name[PURE_MODULE_NAME]; uint8_t type; uint16_t rows; uint32_t offset; } PureField; typedef struct { uint8_t kind; - bool keyed; uint16_t field_start, field_count; uint32_t prefix; /* fixed payload bytes before the variable fields */ uint32_t period; /* a timer's, in milliseconds */ @@ -339,12 +338,11 @@ typedef struct { uint8_t lower_relation, upper_relation; PureBlock lower, upper; _Static_assert(PURE_MODULE_STATES <= 32, "a state is a bit of PureApp.counted"); typedef struct { - uint16_t states, inputs, views, handlers, fields, collections, constants, records, record_fields; + uint16_t states, inputs, views, handlers, fields, constants, records, record_fields; uint32_t code_count; char state_names[PURE_MODULE_STATES][PURE_MODULE_NAME]; char input_names[PURE_MODULE_INPUTS][PURE_MODULE_NAME]; char view_names[PURE_MODULE_VIEWS][PURE_MODULE_NAME]; - char collection_names[PURE_MODULE_COLLECTIONS][PURE_MODULE_NAME]; char input_keys[PURE_MODULE_INPUTS][PURE_MODULE_NAME]; uint8_t state_type[PURE_MODULE_STATES]; uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */ @@ -449,27 +447,13 @@ int pure_module_position(const PureModule *, uint32_t view); * state), or -1: a host may lend its stored extents instead of copying. */ int pure_module_text_state(const PureModule *, uint32_t view); -/* Keyed collections live in a data host (for example the tree store). One - * event opens at most one document, the message's key, at its current - * version; edits stay in that working version until the event commits it. - * A version's bytes field is the value's size. */ +/* A data host keeps the app's states (for example the tree store); without one (all of it NULL) they live in memory + * alone. state opens a runtime-owned value by name as a draft (a missing one is empty at revision 0); save publishes + * the given drafts together, all or nothing. */ typedef struct { void *context; - PureResult (*open)(void *context, uint32_t collection, const char *key, - void **document, int64_t *revision); - PureVersion (*version)(void *context, void *document); - PureResult (*read)(void *context, void *document, PureVersion, size_t offset, size_t length, - bool (*visit)(void *, PureSpan), void *visit_context); - 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 documents and state drafts together, all or - * nothing, and names each document's new revision. */ PureResult (*state)(void *context, const char *name, PureDraft **out); - PureResult (*save)(void *context, void *const *documents, size_t document_count, PureDraft *const *states, - size_t count, int64_t *revisions); + PureResult (*save)(void *context, PureDraft *const *states, size_t count); /* Optional, with state and save: the directory where the app's lists are * kept, their images and the log of their changes (core/log.h); without * one, list states are not stored. */ @@ -477,7 +461,7 @@ typedef struct { } PureData; /* A byte or text value inside one event or view: a range of the payload, a - * document or text version, a module constant, a copy or the event's arena. */ + * text version, a module constant, a copy or the event's arena. */ typedef struct { uint8_t source; uint16_t owner; PureVersion version; uint64_t offset, length; } PureBytes; typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena; @@ -487,13 +471,11 @@ typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena; * (or 0 in an empty list). Every event is atomic: on failure nothing changes. * * Saving. When the data host keeps states the app starts from the stored - * ones. A save publishes, in one commit, the documents edited by messages and - * the states changed since the last save; if it fails, every event since the - * last save is undone. By default each key, text and message event saves - * before it returns (a frame waits for the next one). Deferred, events apply - * at once and a host saves a burst with one commit: an event is durable once - * a later save succeeds, and each key's document joins a burst once (a second - * message for it, or one past PURE_APP_PENDING keys, saves the burst first). */ + * ones. A save publishes, in one commit, the states changed since the last + * save; if it fails, every event since the last save is undone. By default + * each key, text and message event saves before it returns (a frame waits for + * the next one). Deferred, events apply at once and a host saves a burst with + * one commit: an event is durable once a later save succeeds. */ /* The runtime's lists' places among the stored lists' records (PureApp): * the numbers heard, and the outbox. */ enum { PURE_LIST_HEARD = PURE_MODULE_STATES, PURE_LIST_OUTBOX, PURE_LISTS_STORED }; @@ -521,20 +503,12 @@ typedef struct { bool uncertain; /* a save may or may not be stored: none follows until a start */ PureDraft *stored_values; /* numbers, cursors and indices, when kept */ PureVersion saved[PURE_MODULE_STATES]; /* text and list versions at the last save */ - /* Events since the last save: what a failed save returns to, and the - * documents messages opened (held until the save). */ + /* Events since the last save: what a failed save returns to. */ bool deferred, open; PureValue durable_values[PURE_MODULE_STATES]; PureVersion durable_texts[PURE_MODULE_STATES]; size_t durable_lists[PURE_MODULE_STATES]; /* each stored list's journal length */ uint64_t durable_ticks, durable_events; - struct PurePending { void *document; char key[65]; bool edited; } *pending; /* grows to PURE_APP_PENDING */ - unsigned pending_count, pending_capacity; - /* The pending documents by key, so a message finds its key in one look: - * open addressing over the key's hash, a slot holding one more than the - * document's place in pending (0: none). */ - uint16_t pending_index[2 * PURE_APP_PENDING]; - bool document_pending; /* the current event's document is among them */ PureFault fault; uint32_t fault_pc; size_t memory; /* the budget of what the states hold (0: none): pure_app_memory */ @@ -553,12 +527,10 @@ typedef struct { struct PureRows { uint32_t table; uint64_t changes, given; size_t *items, count, capacity; PureFault fault; } *rows; /* a filtered table's items */ unsigned row_tables; /* Current message or text event, kept until pure_app_finish. */ - void *document; PureSpan payload; bool has_reply; PureBytes reply; unsigned char *copied; - int64_t revision; PureArena arena, view_arena; /* Links (runtime-owned, saved with the states): the outbox, messages sent * on outputs and not trimmed, in the order sent, a list of {output (its @@ -612,11 +584,8 @@ PureResult pure_app_start(PureApp *, const PureModule *, PureData data); /* Publish every change since the last save in one commit, or undo them all. */ PureResult pure_app_save(PureApp *); /* Defer saving to pure_app_save (false: save after each event). Needs a data - * host that saves documents together (PureData.save). */ + * host that saves together (PureData.save). */ bool pure_app_defer(PureApp *, bool deferred); -/* Whether key's document is in the unsaved burst: a message for it would - * save the burst first. */ -bool pure_app_pending(const PureApp *, const char *key); /* Leaves unsaved frame changes unsaved: call pure_app_save first to keep them. */ void pure_app_stop(PureApp *); PureResult pure_app_frame(PureApp *); @@ -629,18 +598,14 @@ PureResult pure_app_input(PureApp *, uint32_t input, PureEdge); PureResult pure_app_point(PureApp *, uint32_t state, size_t position); /* Deliver committed text (for example from an input method). Not UTF-8: invalid. */ PureResult pure_app_text(PureApp *, uint32_t input, PureSpan text); -/* Deliver a message. base is the revision the sender edited, or -1 for none; - * a handler that edits the document needs one. payload stays alive until - * pure_app_finish. On return app->revision is the document's revision - * (deferred: the revision the next save gives it). */ -PureResult pure_app_message(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload); +/* Deliver a message. payload stays alive until pure_app_finish. */ +PureResult pure_app_message(PureApp *, uint32_t input, PureSpan payload); /* The same, with the variable fields where the host found them: field f * starts at placed[2f] in payload and has placed[2f + 1] bytes (entries of * fixed fields are not read, nor are the variable fields' lengths in the * prefix). A host that parsed a request in place passes the fields' bytes * where they are. */ -PureResult pure_app_message_fields(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload, - const uint32_t *placed); +PureResult pure_app_message_fields(PureApp *, uint32_t input, PureSpan payload, const uint32_t *placed); /* Visit the reply of the last message in order, without copying, until the * next event (deferred: copy it before a later save too). */ PureResult pure_app_reply(const PureApp *, bool (*visit)(void *, PureSpan), void *context); diff --git a/core/test-module.c b/core/test-module.c index 181f96d..2696e28 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -37,10 +37,10 @@ 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. Arguments * name their view by its number in key. */ -typedef struct { const char *name, *key; int kind; bool keyed; const F *fields; size_t nfields; } I; -#define KEY(name, key) {name, key, 0, false, NULL, 0} +typedef struct { const char *name, *key; int kind; const F *fields; size_t nfields; } I; +#define KEY(name, key) {name, key, 0, NULL, 0} /* A timer's period (milliseconds) rides in nfields. */ -#define TIMER(name, period) {name, NULL, PURE_KIND_TIMER, false, NULL, period} +#define TIMER(name, period) {name, NULL, PURE_KIND_TIMER, NULL, period} /* A number view, or a bool (i64) view when its length carries BOOL_VIEW (I64_VIEW). */ typedef struct { const char *name; uint32_t start, length; } V; #define BOOL_VIEW (1u << 31) @@ -97,7 +97,6 @@ static size_t build(unsigned char *out, Spec spec) { size_t names = 0; for (size_t i = 0; i < spec.ns; ++i) text[names++] = spec.s[i].name; for (size_t i = 0; i < spec.nt; ++i) text[names++] = spec.t[i].name; - for (size_t i = 0; i < spec.nc; ++i) text[names++] = spec.c[i]; for (size_t i = 0; i < spec.ni; ++i) { text[names++] = spec.in[i].name; if (!spec.in[i].kind) text[names++] = spec.in[i].key; @@ -124,17 +123,18 @@ static size_t build(unsigned char *out, Spec spec) { for (size_t i = 0; i < spec.ni; ++i) fields += spec.in[i].kind == PURE_KIND_TIMER ? 0 : spec.in[i].nfields; for (size_t i = 0; i < spec.nk; ++i) { memcpy(blob + blob_used, spec.k[i], strlen(spec.k[i])); blob_used += strlen(spec.k[i]); } unsigned char *p = out; - memset(p, 0, 40); - memcpy(p, "PUREMOD6", 8); + CHECK(!spec.nc); /* (collections are gone: 027's 4b) */ + memset(p, 0, 38); + memcpy(p, "PUREMOD7", 8); size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv + spec.nw; p[8] = (unsigned char)states; p[9] = (unsigned char)(states >> 8); p[10] = (unsigned char)spec.ni; p[12] = (unsigned char)views; p[14] = (unsigned char)spec.nh; put32(p + 16, (uint32_t)spec.no); put32(p + 20, (uint32_t)used); - p[24] = (unsigned char)fields; p[26] = (unsigned char)spec.nc; p[28] = (unsigned char)spec.nk; - p[30] = (unsigned char)spec.nr; p[36] = (unsigned char)record_fields; p[38] = (unsigned char)spec_nfn; - put32(p + 32, (uint32_t)blob_used); - p += 40; + p[24] = (unsigned char)fields; p[26] = (unsigned char)spec.nk; p[28] = (unsigned char)spec.nr; + p[34] = (unsigned char)record_fields; p[36] = (unsigned char)spec_nfn; + put32(p + 30, (uint32_t)blob_used); + p += 38; size_t n = 0; 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) { @@ -150,13 +150,12 @@ static size_t build(unsigned char *out, Spec spec) { p = put32(p, spec.t[i].ref); 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; size_t nf = 0; for (size_t i = 0; i < spec.ni; ++i) { const I *in = &spec.in[i]; p = put32(p, offset[n++]); - *p++ = (unsigned char)(in->kind ? in->kind : 1); *p++ = in->keyed; *p++ = 0; *p++ = 0; + *p++ = (unsigned char)(in->kind ? in->kind : 1); *p++ = 0; *p++ = 0; *p++ = 0; bool timer = in->kind == PURE_KIND_TIMER; p = put32(p, timer ? (uint32_t)in->nfields : in->kind == PURE_KIND_ARGUMENTS ? (uint32_t)strtoul(in->key, NULL, 10) : in->kind ? 0 : offset[n++]); @@ -454,118 +453,7 @@ static void mutations(void) { printf("{\"module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); } -static Spec editor_spec; - -/* An in-memory data host: every splice makes a new immutable version. */ -typedef struct { char key[65]; unsigned char *bytes; size_t size; int64_t revision; } Stored; -typedef struct { - Stored docs[8]; - size_t count, limit; - bool busy; - unsigned opened, closed; -} Memory; -typedef struct { - Memory *memory; - int index; /* -1 for an absent key */ - int64_t base; - unsigned char *versions[64]; - size_t sizes[64], count; -} Doc; - -static Stored *stored(Memory *m, const char *key) { - for (size_t i = 0; i < m->count; ++i) if (!strcmp(m->docs[i].key, key)) return &m->docs[i]; - return NULL; -} - -static PureResult fake_open(void *context, uint32_t collection, const char *key, void **out, int64_t *revision) { - Memory *m = context; - CHECK(collection == 0); - Doc *d = calloc(1, sizeof(*d)); - Stored *s = stored(m, key); - d->memory = m; - d->index = s ? (int)(s - m->docs) : -1; - if (!s) { - CHECK(m->count < 8); - d->index = (int)m->count; - snprintf(m->docs[m->count++].key, 65, "%s", key); - s = &m->docs[d->index]; - } - d->base = s->revision; - d->versions[0] = malloc(s->size + 1); - memcpy(d->versions[0], s->bytes ? s->bytes : (unsigned char *)"", s->size); - d->sizes[0] = s->size; - d->count = 1; - ++m->opened; - *out = d; - *revision = s->revision; - return PURE_OK; -} - -static PureVersion fake_version(void *context, void *document) { - (void)context; - Doc *d = document; - return (PureVersion){d->count - 1, d->sizes[d->count - 1]}; -} - -/* Two spans per read, so replies arrive in pieces. */ -static PureResult fake_read(void *context, void *document, PureVersion at, size_t offset, size_t length, - bool (*visit)(void *, PureSpan), void *visit_context) { - (void)context; - Doc *d = document; - CHECK(at.ref < d->count && at.bytes == d->sizes[at.ref] && offset + length <= at.bytes); - size_t half = length / 2; - if (half && !visit(visit_context, (PureSpan){d->versions[at.ref] + offset, half})) return PURE_OK; - if (length - half) visit(visit_context, (PureSpan){d->versions[at.ref] + offset + half, length - half}); - return PURE_OK; -} - -static PureResult fake_splice(void *context, void *document, size_t offset, size_t remove, PureSpan insert) { - Memory *m = context; - Doc *d = document; - size_t size = d->sizes[d->count - 1], next = size - remove + insert.size; - if (next > m->limit) return PURE_LIMIT; - CHECK(d->count < 64); - unsigned char *bytes = malloc(next + 1); - memcpy(bytes, d->versions[d->count - 1], offset); - if (insert.size) memcpy(bytes + offset, insert.data, insert.size); - memcpy(bytes + offset + insert.size, d->versions[d->count - 1] + offset + remove, size - offset - remove); - d->versions[d->count] = bytes; - d->sizes[d->count++] = next; - return PURE_OK; -} - -static PureResult fake_commit(void *context, void *document, int64_t *revision) { - Memory *m = context; - Doc *d = document; - Stored *s = &m->docs[d->index]; - *revision = s->revision; - if (m->busy) return PURE_BUSY; - if (s->revision != d->base) return PURE_CONFLICT; - free(s->bytes); - s->size = d->sizes[d->count - 1]; - s->bytes = malloc(s->size + 1); - memcpy(s->bytes, d->versions[d->count - 1], s->size); - *revision = ++s->revision; - /* Like a tree draft, a commit rebases: earlier versions are gone. */ - for (size_t i = 0; i + 1 < d->count; ++i) { free(d->versions[i]); d->versions[i] = NULL; d->sizes[i] = 0; } - d->versions[0] = d->versions[d->count - 1]; - d->sizes[0] = d->sizes[d->count - 1]; - d->count = 1; - d->base = s->revision; - return PURE_OK; -} - -static void fake_close(void *context, void *document) { - Memory *m = context; - Doc *d = document; - for (size_t i = 0; i < d->count; ++i) free(d->versions[i]); - free(d); - ++m->closed; -} - -static PureData fake_data(Memory *m) { - return (PureData){m, fake_open, fake_version, fake_read, fake_splice, fake_commit, fake_close, NULL, NULL, NULL}; -} +static Spec message_spec; typedef struct { unsigned char bytes[256]; size_t size, spans; } Collected; @@ -596,113 +484,69 @@ static PureSpan edit_payload(unsigned char *buffer, uint32_t start, uint32_t rem return (PureSpan){buffer, 8 + strlen(insert)}; } -enum { FIELD = PURE_OP_FIELD, DOC = PURE_OP_DOC, SIZE = PURE_OP_DOC_SIZE, LEN_ = PURE_OP_LEN, - REPL = PURE_OP_REPLACE, REPLY = PURE_OP_REPLY, REQ = PURE_OP_REQUIRE, LE = PURE_OP_LE }; +enum { FIELD = PURE_OP_FIELD, LEN_ = PURE_OP_LEN, REPLY = PURE_OP_REPLY, REQ = PURE_OP_REQUIRE, LE = PURE_OP_LE }; -/* The document editor as a module: show replies the document, edit splices - * start .. start + remove with insert, replies insert and counts edits. A - * keep message replies the document read before its own edit; guarded edits - * require the final size to stay within 8 bytes. */ +/* Messages: echo replies its insert; edit adds start + remove to sum, counts itself and replies its insert; guarded + * counts itself if its insert holds 8 bytes at most. A payload that does not hold the fields fails as limit, a failed + * require changes nothing, and a key input takes no message. */ static void messages(void) { static const F edit_fields[] = {{"start", 1}, {"remove", 1}, {"insert", 2}}; static const I inputs[] = { - {"show", NULL, PURE_KIND_MESSAGE, true, NULL, 0}, - {"edit", NULL, PURE_KIND_MESSAGE, true, edit_fields, 3}, - {"keep", NULL, PURE_KIND_MESSAGE, true, edit_fields + 2, 1}, - {"guarded", NULL, PURE_KIND_MESSAGE, true, edit_fields, 3}, + {"echo", NULL, PURE_KIND_MESSAGE, edit_fields + 2, 1}, + {"edit", NULL, PURE_KIND_MESSAGE, edit_fields, 3}, + {"guarded", NULL, PURE_KIND_MESSAGE, edit_fields, 3}, KEY("tick", "Space"), }; - static const S states[] = {{"edits", 0}}; - static const char *const collections[] = {"documents"}; + static const S states[] = {{"edits", 0}, {"sum", 0}}; static const O code[] = { - {DOC, 0, 0}, {REPLY, 0, 0}, /* 0 show */ - {FIELD, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0}, {ADD, 0, 0}, {FIELD, 2, 0}, /* 2 edit */ - {REPL, 0, 0}, {FIELD, 2, 0}, {REPLY, 0, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, - {DOC, 0, 0}, {REPLY, 0, 0}, {C, 0, 0}, {C, 0, 0}, {FIELD, 0, 0}, {REPL, 0, 0}, /* 14 keep: prepend */ - {FIELD, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0}, {ADD, 0, 0}, {FIELD, 2, 0}, /* 20 guarded */ - {REPL, 0, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, - {SIZE, 0, 0}, {C, 0, 8}, {LE, 0, 0}, {REQ, 0, 0}, + {FIELD, 0, 0}, {REPLY, 0, 0}, /* 0 echo */ + {FIELD, 0, 0}, {FIELD, 1, 0}, {ADD, 0, 0}, {L, 1, 0}, {ADD, 0, 0}, {ST, 1, 0}, /* 2 edit */ + {FIELD, 2, 0}, {REPLY, 0, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, + {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 14 guarded */ + {FIELD, 2, 0}, {LEN_, 0, 0}, {C, 0, 8}, {LE, 0, 0}, {REQ, 0, 0}, }; - static const H handlers[] = {{1, 0, 2}, {3, 2, 12}, {5, 14, 6}, {7, 20, 14}}; + static const H handlers[] = {{1, 0, 2}, {3, 2, 12}, {5, 14, 9}}; static const V views[] = {{"count", 0, 0}}; - Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; - editor_spec = spec; + Spec spec = {states, 2, inputs, 4, views, 0, handlers, 3, code, LEN(code), NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + message_spec = spec; PureResult loaded = load(spec, &module_a); if (loaded != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } - Memory memory = {.limit = 100}; PureApp app; - CHECK(pure_app_start(&app, &module_a, fake_data(&memory)) == PURE_OK); + CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK); unsigned char buffer[64]; - - CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_NOT_FOUND && app.revision == 0); - CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "hello")) == PURE_OK); - CHECK(app.revision == 1 && replied(&app, "hello") && app.values[0].num == 1); - CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && app.revision == 1 && replied(&app, "hello")); - /* stale base conflicts before bounds; bounds fail as limit; nothing changes */ - CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 99, 0, "x")) == PURE_CONFLICT && app.revision == 1); - CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 99, 0, "x")) == PURE_LIMIT && app.revision == 1); - CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 2, 9, "x")) == PURE_LIMIT); - CHECK(pure_app_message(&app, 1, "doc", 1, (PureSpan){buffer, 7}) == PURE_LIMIT && app.revision == 0); - CHECK(pure_app_message(&app, 1, "doc", -1, edit_payload(buffer, 0, 0, "x")) == PURE_INVALID); - CHECK(pure_app_message(&app, 1, "bad/key", 1, edit_payload(buffer, 0, 0, "x")) == PURE_INVALID); - CHECK(pure_app_message(&app, 4, "doc", 1, (PureSpan){0}) == PURE_INVALID); - CHECK(memory.docs[0].revision == 1 && memory.docs[0].size == 5 && app.values[0].num == 1); - /* A busy commit changes neither the document nor the states. */ - memory.busy = true; - CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 5, 0, "!")) == PURE_BUSY); - memory.busy = false; - CHECK(memory.docs[0].size == 5 && app.values[0].num == 1 && app.events == 2); - /* The size limit of the data host fails as limit. */ - memory.limit = 6; - CHECK(pure_app_message(&app, 1, "doc", 1, edit_payload(buffer, 5, 0, "!!")) == PURE_LIMIT); - memory.limit = 100; - /* A reply read before the event's own edit keeps its content after commit. */ - CHECK(pure_app_message(&app, 2, "doc", 1, (PureSpan){(const unsigned char *)">> ", 3}) == PURE_OK); - CHECK(app.revision == 2 && replied(&app, "hello")); - CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && replied(&app, ">> hello")); - /* require: an edit that would exceed 8 bytes changes nothing */ - CHECK(pure_app_message(&app, 3, "doc", 2, edit_payload(buffer, 8, 0, "!")) == PURE_LIMIT); - CHECK(memory.docs[0].size == 8 && app.values[0].num == 1); - CHECK(pure_app_message(&app, 3, "doc", 2, edit_payload(buffer, 0, 3, "")) == PURE_OK && app.revision == 3); - CHECK(app.values[0].num == 2); - /* Another key is its own document. */ - CHECK(pure_app_message(&app, 1, "other", 0, edit_payload(buffer, 0, 0, "abc")) == PURE_OK && app.revision == 1); + CHECK(pure_app_message(&app, 0, (PureSpan){(const unsigned char *)"hello", 5}) == PURE_OK && replied(&app, "hello")); + CHECK(pure_app_message(&app, 1, edit_payload(buffer, 2, 3, "xy")) == PURE_OK && replied(&app, "xy")); + CHECK(app.values[0].num == 1 && app.values[1].num == 5); + CHECK(pure_app_message(&app, 1, (PureSpan){buffer, 7}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); + CHECK(pure_app_message(&app, 2, edit_payload(buffer, 0, 0, "123456789")) == PURE_LIMIT); + CHECK(app.values[0].num == 1 && app.values[1].num == 5); + CHECK(pure_app_message(&app, 2, edit_payload(buffer, 0, 0, "12345678")) == PURE_OK && app.values[0].num == 2); + CHECK(pure_app_message(&app, 3, (PureSpan){0}) == PURE_INVALID && app.values[0].num == 2 && app.events == 3); pure_app_stop(&app); - CHECK(memory.opened == memory.closed); - for (size_t i = 0; i < memory.count; ++i) free(memory.docs[i].bytes); /* Verifier rules for typed operations. */ static const O field_in_key[] = {{FIELD, 0, 0}, {ST, 0, 0}}; - static const H key_handler[] = {{9, 0, 2}}; + static const H key_handler[] = {{7, 0, 2}}; Spec bad = spec; bad.o = field_in_key; bad.no = 2; bad.h = key_handler; bad.nh = 1; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message operation outside")); - static const O num_insert[] = {{C, 0, 0}, {C, 0, 0}, {C, 0, 1}, {REPL, 0, 0}}; - static const H edit_handler[] = {{3, 0, 4}}; - bad.o = num_insert; bad.no = 4; bad.h = edit_handler; - CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type")); static const O bytes_state[] = {{FIELD, 2, 0}, {ST, 0, 0}}; - static const H edit_handler2[] = {{3, 0, 2}}; - bad.o = bytes_state; bad.no = 2; bad.h = edit_handler2; + static const H edit_handler[] = {{3, 0, 2}}; + bad.o = bytes_state; bad.no = 2; bad.h = edit_handler; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type")); static const O far_field[] = {{FIELD, 3, 0}, {REPLY, 0, 0}}; - bad.o = far_field; bad.h = edit_handler2; + bad.o = far_field; bad.h = edit_handler; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state, field")); - static const I unkeyed[] = {{"note", NULL, PURE_KIND_MESSAGE, false, NULL, 0}}; - static const O doc_read[] = {{DOC, 0, 0}, {REPLY, 0, 0}}; - static const H note_handler[] = {{1, 0, 2}}; - bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; - CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "document operation outside")); static const F unknown_type[] = {{"insert", 8}, {"start", 1}}; - static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, true, unknown_type, 2}}; - bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, unknown_type, 2}}; + bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message field")); static const H even[] = {{2, 0, 0}}; - bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "handler event")); static const V field_view[] = {{"v", 0, 1}}; static const O field_only[] = {{FIELD, 0, 0}}; - bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect")); /* A value may cross a join when every path brings the same stack. */ static const O branch_value[] = {{C, 0, 1}, {C, 1, 1}, {JZ, 3, 0}, {ST, 0, 0}}; @@ -718,7 +562,7 @@ static void messages(void) { /* Damaged message modules either fail to load or run messages safely. */ static void message_mutations(void) { static unsigned char valid[65536], bytes[65536]; - size_t size = build(valid, editor_spec); + size_t size = build(valid, message_spec); uint32_t state = 0x9e3779b9u; unsigned accepted = 0; for (unsigned round = 0; round < 20000; ++round) { @@ -728,19 +572,17 @@ static void message_mutations(void) { bytes[next_random(&state) % size] ^= (unsigned char)(1u << (next_random(&state) % 8)); if (pure_module_load(bytes, size, &module_b, error, sizeof(error)) != PURE_OK) continue; ++accepted; - Memory memory = {.limit = 64}; PureApp app; - CHECK(pure_app_start(&app, &module_b, fake_data(&memory)) == PURE_OK); + CHECK(pure_app_start(&app, &module_b, (PureData){0}) == PURE_OK); unsigned char payload[32]; for (unsigned e = 0; e < 12; ++e) { for (size_t k = 0; k < sizeof(payload); ++k) payload[k] = (unsigned char)next_random(&state); put32(payload, next_random(&state) % 12); put32(payload + 4, next_random(&state) % 4); PureSpan span = {payload, next_random(&state) % sizeof(payload)}; - uint32_t input = next_random(&state) % 5; + uint32_t input = next_random(&state) % module_b.inputs; if (module_b.input[input].kind == PURE_KIND_MESSAGE) { - int64_t base = (int64_t)(next_random(&state) % 4) - 1; - if (pure_app_message(&app, input, "doc", base, span) == PURE_OK) { + if (pure_app_message(&app, input, span) == PURE_OK) { size_t reply = 0; CHECK(pure_app_reply(&app, count_bytes, &reply) == PURE_OK && reply <= 256); } @@ -749,8 +591,6 @@ static void message_mutations(void) { for (uint32_t s = 0; s < module_b.states; ++s) CHECK(isfinite(app.values[s].num)); } pure_app_stop(&app); - CHECK(memory.opened == memory.closed); - for (size_t i = 0; i < memory.count; ++i) free(memory.docs[i].bytes); } printf("{\"message_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); } @@ -814,7 +654,7 @@ static void texts(void) { "\xeb\xa9\x94\xeb\xaa\xa8", "XYZ", " \xc2\xb7 ", "\xec\x9e\x90"}; static const I inputs[] = { - {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("left", "ArrowLeft"), KEY("right", "ArrowRight"), + {"typed", NULL, PURE_KIND_TEXT, NULL, 0}, KEY("left", "ArrowLeft"), KEY("right", "ArrowRight"), KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("back", "Backspace"), KEY("home", "Home"), KEY("bad", "KeyB"), }; static const O code[] = { @@ -2126,7 +1966,7 @@ static void lists(void) { static const R records[] = {{"task", task_fields, 2}}; static const char *const constants[] = {"", "new", "x"}; static const I inputs[] = { - {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("ins", "KeyI"), KEY("del", "Delete"), + {"typed", NULL, PURE_KIND_TEXT, NULL, 0}, KEY("ins", "KeyI"), KEY("del", "Delete"), KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("toggle", "Space"), KEY("rename", "KeyR"), KEY("bad", "KeyB"), }; static const O code[] = { @@ -2334,7 +2174,7 @@ static void tables(void) { static const RF task_fields[] = {{"title", 1, 0}, {"done", 2, 0}}; static const R records[] = {{"task", task_fields, 2}}; static const char *const constants[] = {"", "x", " ", "empty"}; - static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "Space")}; + static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, NULL, 0}, KEY("toggle", "Space")}; static const O code[] = { {TI, 0, 0}, {C, 1, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, /* 0 typed */ {XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 5 toggle */ @@ -2745,32 +2585,19 @@ static void stored_frames(void) { 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. */ +/* A message's changes are stored with its event, and one that fails stores nothing. */ static void stored_messages(void) { char *path = new_store(); - CHECK(load(editor_spec, &module_a) == PURE_OK); + CHECK(load(message_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].num == 1); - CHECK(pure_app_message(&app, 1, "doc", 0, edit_payload(buffer, 0, 0, "x")) == PURE_CONFLICT && app.values[0].num == 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].num == 1); - CHECK(pure_app_message(&app, 3, "doc", 1, edit_payload(buffer, 5, 0, "!")) == PURE_OK && app.values[0].num == 2); + CHECK(pure_app_message(&app, 1, edit_payload(buffer, 2, 3, "hello")) == PURE_OK && replied(&app, "hello")); + CHECK(pure_app_message(&app, 2, edit_payload(buffer, 0, 0, "-world, long")) == PURE_LIMIT && app.values[0].num == 1); + CHECK(pure_app_message(&app, 2, edit_payload(buffer, 0, 0, "!")) == PURE_OK && app.values[0].num == 2); pure_app_stop(&app); - CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0].num == 2); - CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_OK && app.revision == 2 && replied(&app, "hello!")); + CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0].num == 2 && app.values[1].num == 5); 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); } @@ -3051,82 +2878,12 @@ static void deferred_bursts(void) { remove_store(path); } -/* Messages in a burst: documents of different keys commit together with the - * states, each moving one revision; a second message for a key saves the - * burst first, and so does a full burst. */ -static void deferred_messages(void) { - char *path = new_store(); - CHECK(load(editor_spec, &module_a) == PURE_OK); - PureApp app; - unsigned char buffer[64]; - char key[16]; - CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pure_app_defer(&app, true)); - for (int i = 0; i < 3; ++i) { - snprintf(key, sizeof(key), "k%d", i); - CHECK(pure_app_message(&app, 1, key, 0, edit_payload(buffer, 0, 0, "ab")) == PURE_OK && app.revision == 1); - } - CHECK(app.values[0].num == 3 && app.pending_count == 3 && app.open); - /* The same key again: the burst is saved first, then this edit joins a new one. */ - CHECK(pure_app_message(&app, 1, "k0", 1, edit_payload(buffer, 2, 0, "c")) == PURE_OK && app.revision == 2); - CHECK(app.pending_count == 1 && app.values[0].num == 4); - CHECK(pure_app_message(&app, 1, "k1", 0, edit_payload(buffer, 0, 0, "x")) == PURE_CONFLICT); - CHECK(pure_app_save(&app) == PURE_OK && !app.open && !app.pending_count); - for (int i = 0; i < PURE_APP_PENDING + 1; ++i) { - snprintf(key, sizeof(key), "m%d", i); - CHECK(pure_app_message(&app, 1, key, 0, edit_payload(buffer, 0, 0, "y")) == PURE_OK); - CHECK(app.pending_count == (i < PURE_APP_PENDING ? (unsigned)i + 1 : 1)); /* full: saved on the way */ - } - pure_app_stop(&app); /* the last edit was never saved */ - CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && app.values[0].num == 4 + PURE_APP_PENDING); - CHECK(pure_app_message(&app, 0, "k0", -1, (PureSpan){0}) == PURE_OK && app.revision == 2 && replied(&app, "abc")); - snprintf(key, sizeof(key), "m%d", PURE_APP_PENDING); - CHECK(pure_app_message(&app, 0, key, -1, (PureSpan){0}) == PURE_NOT_FOUND); - snprintf(key, sizeof(key), "m%d", PURE_APP_PENDING - 1); - CHECK(pure_app_message(&app, 0, key, -1, (PureSpan){0}) == PURE_OK && replied(&app, "y")); - pure_app_stop(&app); - remove_store(path); -} - -/* The burst's keys are found in one look (an index over their hashes, most - * slots shared at 200 keys): every pending key and no other, reads as well - * as edits, and a message for one of them saves the burst first. */ -static void pending_keys(void) { - char *path = new_store(); - CHECK(load(editor_spec, &module_a) == PURE_OK); - PureApp app; - unsigned char buffer[64]; - char key[16]; - CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pure_app_defer(&app, true)); - for (int i = 0; i < 200; ++i) { - snprintf(key, sizeof(key), "p%d", i); - CHECK(!pure_app_pending(&app, key)); - CHECK(pure_app_message(&app, 1, key, 0, edit_payload(buffer, 0, 0, "q")) == PURE_OK); - } - CHECK(pure_app_save(&app) == PURE_OK && !pure_app_pending(&app, "p0")); - for (int i = 0; i < 200; ++i) { - snprintf(key, sizeof(key), "p%d", i); - CHECK((i % 2 ? pure_app_message(&app, 0, key, -1, (PureSpan){0}) - : pure_app_message(&app, 1, key, 1, edit_payload(buffer, 1, 0, "r"))) == PURE_OK); - } - for (int i = 0; i < 210; ++i) { - snprintf(key, sizeof(key), "p%d", i); - CHECK(pure_app_pending(&app, key) == (i < 200)); - } - CHECK(!pure_app_pending(&app, "p") && !pure_app_pending(&app, "p1000") && app.pending_count == 200); - CHECK(pure_app_message(&app, 0, "p150", -1, (PureSpan){0}) == PURE_OK && app.pending_count == 1); - CHECK(pure_app_pending(&app, "p150") && !pure_app_pending(&app, "p151")); - pure_app_stop(&app); - remove_store(path); -} - static void deferred(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", ""}; deferred_equivalence(list_spec, list_words, LEN(list_words), 0x2545f491u); deferred_equivalence(text_spec, text_words, LEN(text_words), 0x1b873593u); deferred_bursts(); - deferred_messages(); - pending_keys(); } /* Text inserted at a cursor goes before it; at a mark (a cursor that stays), @@ -3135,7 +2892,7 @@ static void marks(void) { enum { MARK = 2 | PURE_CURSOR_STAYS << 8 }; static const T textual[] = {{"note", 1, 0}, {"at", 2, 0}, {"mark", MARK, 0}}; static const char *const constants[] = {"ab"}; - static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("keep", "KeyK"), KEY("back", "Backspace")}; + static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, NULL, 0}, KEY("keep", "KeyK"), KEY("back", "Backspace")}; static const O code[] = { {CUL, 1, 0}, {TI, 0, 0}, {TINS, 0, 0}, /* 0 typed: at the cursor */ {CUL, 1, 0}, {CS, 2, 0}, /* 3 keep: mark = at */ @@ -3174,7 +2931,7 @@ static const RF fold_fields[] = {{"title", 1, 0}, {"done", 2, 0}, {"points", 0, static const R fold_records[] = {{"task", fold_fields, 3}}; static const T fold_states[] = {{"tasks", 3, 0}, {"at", 4, 0}}; static const char *const fold_constants[] = {""}; -static const I fold_inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "KeyT")}; +static const I fold_inputs[] = {{"typed", NULL, PURE_KIND_TEXT, NULL, 0}, KEY("toggle", "KeyT")}; enum { F_TITLE = 0, F_DONE = 1u << 16, F_POINTS = 2u << 16 }; static const O fold_code[] = { {TI, 0, 0}, {C, 1, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */ @@ -3656,8 +3413,8 @@ static const RF stock_fields[] = {{"item", 0, 0}, {"left", 0, 0}}, line_fields[] entry_fields[] = {{"item", 0, 0}, {"qty", 0, 0}}; static const R walk_records[] = {{"Stock", stock_fields, 2}, {"Line", line_fields, 2}, {"Entry", entry_fields, 2}}; static const F order_fields[] = {{"lines", PURE_FIELD_ROWS | W_LINES << 8}}, restock_fields[] = {{"item", 1}, {"left", 1}}; -static const I walk_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, order_fields, 1}, KEY("clean", "KeyC"), - {"restock", NULL, PURE_KIND_MESSAGE, false, restock_fields, 2}}; +static const I walk_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, order_fields, 1}, KEY("clean", "KeyC"), + {"restock", NULL, PURE_KIND_MESSAGE, restock_fields, 2}}; static const O walk_code[] = { /* 0 order: for l in order.lines { stock[l.item].left -= l.qty (not below 0); log.append(l) } */ {EACH, WALKING(W_LINES, 20), 0}, @@ -3730,7 +3487,7 @@ static void walks(void) { unsigned char payload[8]; put32(payload, (uint32_t)item); put32(payload + 4, (uint32_t)left); - result = pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}); + result = pure_app_message(&app, 2, (PureSpan){payload, 8}); size_t at = 0; while (at < shop.stocked && shop.stock[at].item < item) ++at; if (at < shop.stocked && shop.stock[at].item == item) ok = false; @@ -3756,7 +3513,7 @@ static void walks(void) { } for (size_t k = 0; !ok && k < lines; ++k) /* the rest of the rows, for the payload */ if (size < (k + 1) * 16) size += put_row(payload + size, (double)(next_random(&seed) % 13), 1); - result = pure_app_message(&app, 0, NULL, -1, (PureSpan){size ? payload : NULL, size}); + result = pure_app_message(&app, 0, (PureSpan){size ? payload : NULL, size}); if (ok) { shop.n += 1; shop.sum += qty_total; @@ -3804,13 +3561,13 @@ static void walks(void) { unsigned char short_row[12] = {0}, not_finite[16]; put_double(not_finite, 1); put_double(not_finite + 8, NAN); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){short_row, 12}) == PURE_LIMIT && same_shop(&app, &before)); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){not_finite, 16}) == PURE_LIMIT && same_shop(&app, &before)); + CHECK(pure_app_message(&app, 0, (PureSpan){short_row, 12}) == PURE_LIMIT && same_shop(&app, &before)); + CHECK(pure_app_message(&app, 0, (PureSpan){not_finite, 16}) == PURE_LIMIT && same_shop(&app, &before)); pure_app_stop(&app); /* What a walk and a rows field may not be. */ static const F note_fields[] = {{"lines", PURE_FIELD_ROWS | W_LINES << 8}, {"text", 4}}; - static const I note_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, note_fields, 2}}; + static const I note_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, note_fields, 2}}; static const O into_walked[] = {{EACH, WALKING(W_STOCK, 3), 0}, {C, 0, 1}, {C, 0, 2}, {LA, W_STOCK, 0}}; static const O nested[] = {{EACH, WALKING(W_STOCK, 4), 0}, {EACH, WALKING(W_LOG, 3), 0}, {C, 0, 1}, {C, 0, 2}, {LA, W_STOCK, 0}}; @@ -3824,8 +3581,8 @@ static void walks(void) { static const V walk_view[] = {{"v", 0, 4}}; static const T unnamed[] = {{"stock", 3 | 1 << 8, 0}, {"lines", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, {"log", 3, 2}}; static const F logged_rows[] = {{"lines", PURE_FIELD_ROWS | W_LOG << 8}}; - static const I logged_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, logged_rows, 1}}; - static const I sent_rows[] = {{"order", NULL, PURE_KIND_OUTPUT, false, order_fields, 1}}; + static const I logged_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, logged_rows, 1}}; + static const I sent_rows[] = {{"order", NULL, PURE_KIND_OUTPUT, order_fields, 1}}; const struct { Spec spec; const char *reason; } refused[] = { {walk_spec(into_walked, LEN(into_walked), note_inputs, 1, first3, 1, walk_lists, NULL, 0), "insert into the list walked"}, {walk_spec(nested, LEN(nested), note_inputs, 1, first5, 1, walk_lists, NULL, 0), "insert into the list walked"}, @@ -3871,7 +3628,7 @@ static void walk_mutations(void) { for (size_t k = 0; k + 16 <= length && next_random(&state) % 2; k += 16) put_row(payload + k, (double)(next_random(&state) % 8), (double)(next_random(&state) % 3)); if (module_b.input[input].kind == PURE_KIND_MESSAGE) - pure_app_message(&app, input, NULL, -1, (PureSpan){payload, length}); + pure_app_message(&app, input, (PureSpan){payload, length}); else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN); for (uint32_t s_ = 0; s_ < module_b.states; ++s_) @@ -3897,11 +3654,11 @@ static const R argued_records[] = {{"Account", account_fields, 3}}; static const F pay_fields[] = {{"b", 1}}, add_fields[] = {{"aid", 1}, {"bid", 1}, {"balance", 3}}, aid_fields[] = {{"aid", 1}}, min_fields[] = {{"min", 3}}, name_fields[] = {{"name", 4}}, b_fields[] = {{"b", 1}}; static const I argued_inputs[] = { - {"pay", NULL, PURE_KIND_MESSAGE, false, pay_fields, 1}, {"add", NULL, PURE_KIND_MESSAGE, false, add_fields, 3}, - {"balance.args", "0", PURE_KIND_ARGUMENTS, false, aid_fields, 1}, - {"rich.args", "1", PURE_KIND_ARGUMENTS, false, min_fields, 1}, - {"echo.args", "2", PURE_KIND_ARGUMENTS, false, name_fields, 1}, - {"branch.args", "3", PURE_KIND_ARGUMENTS, false, b_fields, 1}}; + {"pay", NULL, PURE_KIND_MESSAGE, pay_fields, 1}, {"add", NULL, PURE_KIND_MESSAGE, add_fields, 3}, + {"balance.args", "0", PURE_KIND_ARGUMENTS, aid_fields, 1}, + {"rich.args", "1", PURE_KIND_ARGUMENTS, min_fields, 1}, + {"echo.args", "2", PURE_KIND_ARGUMENTS, name_fields, 1}, + {"branch.args", "3", PURE_KIND_ARGUMENTS, b_fields, 1}}; static const O argued_code[] = { /* 0 balance(aid) = accounts[aid].balance */ {FIELD, 0, 0}, {PURE_OP_LIST_FIND, A_ACCOUNTS, 0}, {IG, ITEM(A_ACCOUNTS, 2), 0}, @@ -3949,7 +3706,7 @@ static void view_arguments(void) { put32(payload, aid); put32(payload + 4, aid % 3); put_double(payload + 8, aid * 10.0); - CHECK(pure_app_message(&app, 1, NULL, -1, (PureSpan){payload, 16}) == PURE_OK); + CHECK(pure_app_message(&app, 1, (PureSpan){payload, 16}) == PURE_OK); } double value; unsigned char four[8]; @@ -3985,17 +3742,17 @@ static void view_arguments(void) { } } put32(four, 2); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){four, 4}) == PURE_OK && app.values[A_N].num == 10); + CHECK(pure_app_message(&app, 0, (PureSpan){four, 4}) == PURE_OK && app.values[A_N].num == 10); pure_app_stop(&app); /* What arguments may not be. */ - static const I on_column[] = {{"pay", NULL, PURE_KIND_MESSAGE, false, pay_fields, 1}, - {"aid.args", "4", PURE_KIND_ARGUMENTS, false, aid_fields, 1}}; - static const I misnamed[] = {{"pay", NULL, PURE_KIND_MESSAGE, false, pay_fields, 1}, - {"balance.argz", "0", PURE_KIND_ARGUMENTS, false, aid_fields, 1}}; - static const I twice[] = {{"pay", NULL, PURE_KIND_MESSAGE, false, pay_fields, 1}, - {"balance.args", "0", PURE_KIND_ARGUMENTS, false, aid_fields, 1}, - {"balance.args", "0", PURE_KIND_ARGUMENTS, false, aid_fields, 1}}; + static const I on_column[] = {{"pay", NULL, PURE_KIND_MESSAGE, pay_fields, 1}, + {"aid.args", "4", PURE_KIND_ARGUMENTS, aid_fields, 1}}; + static const I misnamed[] = {{"pay", NULL, PURE_KIND_MESSAGE, pay_fields, 1}, + {"balance.argz", "0", PURE_KIND_ARGUMENTS, aid_fields, 1}}; + static const I twice[] = {{"pay", NULL, PURE_KIND_MESSAGE, pay_fields, 1}, + {"balance.args", "0", PURE_KIND_ARGUMENTS, aid_fields, 1}, + {"balance.args", "0", PURE_KIND_ARGUMENTS, aid_fields, 1}}; static const H handled[] = {{5, 31, 4}}; const struct { Spec spec; const char *reason; } refused[] = { {argued_spec(on_column, 2, NULL, 0), "arguments"}, @@ -4024,7 +3781,7 @@ static const T local_lists[] = {{"xs", 3, 0}}; static const RF local_fields[] = {{"a", 0, 0}}; static const R local_records[] = {{"X", local_fields, 1}}; static const F local_add_fields[] = {{"a", 3}}; -static const I local_inputs[] = {KEY("sum", "KeyS"), {"add", NULL, PURE_KIND_MESSAGE, false, local_add_fields, 1}}; +static const I local_inputs[] = {KEY("sum", "KeyS"), {"add", NULL, PURE_KIND_MESSAGE, local_add_fields, 1}}; static const O local_code[] = { /* 0 on sum: total = total + x.a for each x (total starts at 0); n = count(x in xs where x.a < total); * last = xs.last; m = total + last.a (every event: total is 0 again) */ @@ -4056,7 +3813,7 @@ static void locals(void) { unsigned char payload[8]; items[count] = (double)(next_random(&seed) % 20) - 5; put_double(payload, items[count++]); - CHECK(pure_app_message(&app, 1, NULL, -1, (PureSpan){payload, 8}) == PURE_OK); + CHECK(pure_app_message(&app, 1, (PureSpan){payload, 8}) == PURE_OK); CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); double total = 1, below = 0; for (size_t i = 0; i < count; ++i) total += items[i]; @@ -4103,7 +3860,7 @@ static const T range_lists[] = {{"l", 3 | 1 << 8, 0}}; static const RF range_fields[] = {{"key", 0, 0}, {"a", 0, 0}, {"b", 0, 0}}; static const R range_records[] = {{"K", range_fields, 3}}; static const F bounds_fields[] = {{"lo", 3}, {"hi", 3}}; -static const I range_inputs[] = {{"bounds", NULL, PURE_KIND_MESSAGE, false, bounds_fields, 2}, KEY("walk", "KeyW"), +static const I range_inputs[] = {{"bounds", NULL, PURE_KIND_MESSAGE, bounds_fields, 2}, KEY("walk", "KeyW"), KEY("drift", "KeyD"), KEY("slice", "KeyS")}; #define KEYED(field) {IG, ITEM(R_L, field), 0} static const O range_code[] = { @@ -4190,8 +3947,8 @@ static void ranges(void) { double lo = (double)(next_random(&seed) % 900) - 50, hi = lo + (double)(next_random(&seed) % 400) - 20; put_double(payload, lo); put_double(payload + 8, hi); - CHECK(pure_app_message(&a, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_OK && - pure_app_message(&b, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_OK); + CHECK(pure_app_message(&a, 0, (PureSpan){payload, 16}) == PURE_OK && + pure_app_message(&b, 0, (PureSpan){payload, 16}) == PURE_OK); /* The slice: its items alone, a division by zero among them failing it. */ size_t sliced = 0; bool divided = true; @@ -4312,9 +4069,9 @@ static const RF score_fields[] = {{"score", 0, 0}}, flag_fields[] = {{"on", 2, 0 static const R keyed_records[] = {{"person", person_fields, 2}, {"score", score_fields, 1}, {"flag", flag_fields, 1}}; static const char *const keyed_constants[] = {"", "bob", "m"}; static const I keyed_inputs[] = { - {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, {"seek", NULL, PURE_KIND_TEXT, false, NULL, 0}, - {"has", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("drop", "Delete"), KEY("bump", "KeyB"), - KEY("two", "KeyN"), KEY("zero", "KeyO"), KEY("minus_zero", "KeyM"), {"twice", NULL, PURE_KIND_TEXT, false, NULL, 0}, + {"typed", NULL, PURE_KIND_TEXT, NULL, 0}, {"seek", NULL, PURE_KIND_TEXT, NULL, 0}, + {"has", NULL, PURE_KIND_TEXT, NULL, 0}, KEY("drop", "Delete"), KEY("bump", "KeyB"), + KEY("two", "KeyN"), KEY("zero", "KeyO"), KEY("minus_zero", "KeyM"), {"twice", NULL, PURE_KIND_TEXT, NULL, 0}, }; static const O keyed_code[] = { {TI, 0, 0}, {C, 0, 0}, {LA, PEOPLE, 0}, {TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}, /* 0 typed: add, point at it */ @@ -4799,7 +4556,7 @@ static void message_fields(void) { static const char *const constants[] = {""}; static const F fields[] = {{"title", PURE_FIELD_TEXT}, {"points", PURE_FIELD_NUMBER}, {"note", PURE_FIELD_BYTES}, {"count", PURE_FIELD_U32}}; - static const I inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, false, fields, 4}}; + static const I inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, fields, 4}}; static const O code[] = {{FIELD, 1, 0}, {FIELD, 3, 0}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0}, {FIELD, 2, 0}, {REPLY, 0, 0}}; static const H handlers[] = {{1, 0, 8}}; @@ -4820,28 +4577,28 @@ static void message_fields(void) { memcpy(payload + 12, &count, 4); memcpy(payload + 16, "\xea\xb0\x80\xeb\x82\x98" "xyz", 9); size = 25; - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, size}) == PURE_OK); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, size}) == PURE_OK); CHECK(app.values[0].num == 5.5 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\xeb\x82\x98") && replied(&app, "xyz")); pure_app_finish(&app); /* Empty variable fields. */ title_size = 0; memcpy(payload, &title_size, 4); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_OK && !*state_text(&app, 1) && replied(&app, "")); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, 16}) == PURE_OK && !*state_text(&app, 1) && replied(&app, "")); pure_app_finish(&app); /* A title longer than what is there, a title that is not UTF-8, a number that is not finite, a short prefix. */ title_size = 10; memcpy(payload, &title_size, 4); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 25}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, 25}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); title_size = 2; memcpy(payload, &title_size, 4); memcpy(payload + 16, "\xff\xfe", 2); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 18}) == PURE_INVALID && app.fault == PURE_FAULT_ENCODING); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, 18}) == PURE_INVALID && app.fault == PURE_FAULT_ENCODING); title_size = 0; points = NAN; memcpy(payload, &title_size, 4); memcpy(payload + 4, &points, 8); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_INVALID && app.fault == PURE_FAULT_NON_FINITE); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, 15}) == PURE_LIMIT); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, 16}) == PURE_INVALID && app.fault == PURE_FAULT_NON_FINITE); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, 15}) == PURE_LIMIT); CHECK(app.values[0].num == 5.5 && !*state_text(&app, 1)); /* failed messages changed nothing */ pure_app_stop(&app); /* A text field is text and a bytes field bytes. */ @@ -4867,18 +4624,18 @@ static void message_fields(void) { memcpy(payload + 16, body, sizeof(body) - 1); size = 16 + sizeof(body) - 1; uint32_t placed[8] = {16 + 23, 6, 0, 0, 16 + 9, 3, 0, 0}; - CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_OK); + CHECK(pure_app_message_fields(&app, 0, (PureSpan){payload, size}, placed) == PURE_OK); CHECK(app.values[0].num == 5.5 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\xeb\x82\x98") && replied(&app, "xyz")); pure_app_finish(&app); /* A field that starts or ends past the payload, or a title that is not UTF-8 there. */ placed[0] = (uint32_t)size + 1; - CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_LIMIT); + CHECK(pure_app_message_fields(&app, 0, (PureSpan){payload, size}, placed) == PURE_LIMIT); placed[0] = (uint32_t)size - 2; placed[1] = 3; - CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_LIMIT); + CHECK(pure_app_message_fields(&app, 0, (PureSpan){payload, size}, placed) == PURE_LIMIT); placed[0] = 16 + 24; placed[1] = 2; - CHECK(pure_app_message_fields(&app, 0, NULL, -1, (PureSpan){payload, size}, placed) == PURE_INVALID && + CHECK(pure_app_message_fields(&app, 0, (PureSpan){payload, size}, placed) == PURE_INVALID && app.fault == PURE_FAULT_ENCODING); CHECK(app.values[0].num == 5.5); pure_app_stop(&app); @@ -4886,7 +4643,7 @@ static void message_fields(void) { /* Fields in groups: a name is a path of identifiers (user.name), and * never both a field and a group. */ static const F grouped[] = {{"user.name", PURE_FIELD_TEXT}, {"user.age", PURE_FIELD_U32}, {"plan", PURE_FIELD_TEXT}}; - static const I grouped_inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, false, grouped, 3}}; + static const I grouped_inputs[] = {{"post", NULL, PURE_KIND_MESSAGE, grouped, 3}}; static const O grouped_code[] = {{FIELD, 1, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0}}; static const H grouped_handlers[] = {{1, 0, 4}}; Spec groups = spec; @@ -4896,12 +4653,12 @@ static void message_fields(void) { for (size_t i = 0; i < LEN(refused_paths); ++i) { F renamed[3] = {grouped[0], grouped[1], grouped[2]}; renamed[0].name = refused_paths[i]; - I renamed_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, false, renamed, 3}}; + I renamed_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, renamed, 3}}; groups.in = renamed_inputs; CHECK(load(groups, &module_b) == PURE_INVALID && strstr(error, "message field")); } F both[3] = {{"user", PURE_FIELD_TEXT}, grouped[1], grouped[2]}; - I both_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, false, both, 3}}; + I both_inputs[1] = {{"post", NULL, PURE_KIND_MESSAGE, both, 3}}; groups.in = both_inputs; CHECK(load(groups, &module_b) == PURE_INVALID && strstr(error, "field and group")); } @@ -4944,7 +4701,7 @@ static PureResult buy(PureApp *app, uint32_t input, const char *item, uint32_t c size_t length = strlen(item); memcpy(payload, &count, 4); memcpy(payload + 4, item, length); - PureResult result = pure_app_message(app, input, NULL, -1, (PureSpan){payload, 4 + length}); + PureResult result = pure_app_message(app, input, (PureSpan){payload, 4 + length}); pure_app_finish(app); return result; } @@ -4966,9 +4723,9 @@ static void links(void) { static const S sender_nums[] = {{"n", 0}}; static const F order_fields[] = {{"item", PURE_FIELD_TEXT}, {"count", PURE_FIELD_U32}}; static const F ordered_fields[] = {{"item", PURE_FIELD_TEXT}, {"count", PURE_FIELD_U32}, {"total", PURE_FIELD_NUMBER}}; - static const I sender_inputs[] = {{"buy", NULL, PURE_KIND_MESSAGE, false, order_fields, 2}, - {"fail", NULL, PURE_KIND_MESSAGE, false, order_fields, 2}, - {"ordered", NULL, PURE_KIND_OUTPUT, false, ordered_fields, 3}}; + static const I sender_inputs[] = {{"buy", NULL, PURE_KIND_MESSAGE, order_fields, 2}, + {"fail", NULL, PURE_KIND_MESSAGE, order_fields, 2}, + {"ordered", NULL, PURE_KIND_OUTPUT, ordered_fields, 3}}; static const O sender_code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0}, {L, 0, 0}, {SEND, 2, 0}, /* buy */ {FIELD, 0, 0}, {FIELD, 1, 0}, {C, 0, 0}, {SEND, 2, 0}, {C, 1, 0}, {REQ, 0, 0}}; /* fail */ @@ -4978,7 +4735,7 @@ static void links(void) { static const S receiver_nums[] = {{"stock", 0}}; static const T receiver_texts[] = {{"last", 1, 0}}; static const char *const receiver_constants[] = {""}; - static const I receiver_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, ordered_fields, 3}}; + static const I receiver_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, ordered_fields, 3}}; static const O receiver_code[] = {{FIELD, 1, 0}, {L, 0, 0}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {TSET, 1, 0}, {FIELD, 1, 0}, {C, 0, 100}, {LT, 0, 0}, {REQ, 0, 0}}; /* refuses 100 or more */ static const H receiver_handlers[] = {{1, 0, 10}}; @@ -5007,7 +4764,7 @@ static void links(void) { /* Bad deliveries change nothing. */ CHECK(deliver(&b, "", 4, "x", 1) == PURE_INVALID && deliver(&b, "a b", 4, "x", 1) == PURE_INVALID); CHECK(deliver(&b, "other", 0, "x", 1) == PURE_INVALID && pure_app_deliver(&a, 2, "s", 1, sent, NULL) == PURE_INVALID); - CHECK(pure_app_message(&a, 2, NULL, -1, sent) == PURE_INVALID); + CHECK(pure_app_message(&a, 2, sent) == PURE_INVALID); /* Trimmed, a send is gone at once for the host and from the store at the next save. */ pure_app_trim(&a, 2, 1); CHECK(outbox_of(&a).count == 0 && pure_app_save(&a) == PURE_OK && pure_list_count(a.outbox) == 0); @@ -5167,7 +4924,7 @@ static void links(void) { /* Replacing: a module without the output is refused while sends are left * on it; once they are trimmed it replaces, and the output's number is * kept for a module that has it again. */ - static const I plain_inputs[] = {{"buy", NULL, PURE_KIND_MESSAGE, false, order_fields, 2}}; + static const I plain_inputs[] = {{"buy", NULL, PURE_KIND_MESSAGE, order_fields, 2}}; static const O plain_code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}}; static const H plain_handlers[] = {{1, 0, 4}}; Spec plain = {sender_nums, 1, plain_inputs, 1, NULL, 0, plain_handlers, 1, plain_code, LEN(plain_code), @@ -5269,7 +5026,7 @@ static void work_budget(void) { unsigned char payload[8]; for (uint32_t item = 0; item < 5; ++item) { put32(put32(payload, item), 2); - CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_OK && app.work == 3); + CHECK(pure_app_message(&app, 2, (PureSpan){payload, 8}) == PURE_OK && app.work == 3); } pure_app_work(&app, 23 + 22 * 5 - 1); CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_LIMIT && app.fault == PURE_FAULT_WORK); @@ -5279,10 +5036,10 @@ static void work_budget(void) { for (size_t i = 0; i < 5; ++i) CHECK(pure_list_get(app.lists[W_STOCK], i, 1).num == 3); pure_app_work(&app, 2); put32(put32(payload, 9), 1); - CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_LIMIT && app.fault == PURE_FAULT_WORK); + CHECK(pure_app_message(&app, 2, (PureSpan){payload, 8}) == PURE_LIMIT && app.fault == PURE_FAULT_WORK); CHECK(pure_list_count(app.lists[W_STOCK]) == 5); pure_app_work(&app, 0); /* the default */ - CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_OK && pure_list_count(app.lists[W_STOCK]) == 6); + CHECK(pure_app_message(&app, 2, (PureSpan){payload, 8}) == PURE_OK && pure_list_count(app.lists[W_STOCK]) == 6); pure_app_stop(&app); pure_module_release(&module_a); @@ -5332,9 +5089,9 @@ static void memory_budget(void) { static const R records[] = {{"row", fields, 1}}; static const F value[] = {{"value", PURE_FIELD_NUMBER}}; static const I inputs[] = {KEY("add", "KeyA"), KEY("drop", "KeyD"), KEY("send", "KeyS"), - {"out", NULL, PURE_KIND_OUTPUT, false, value, 1}, - {"take", NULL, PURE_KIND_MESSAGE, false, value, 1}, - {"answer", NULL, PURE_KIND_MESSAGE, false, value, 1}}; + {"out", NULL, PURE_KIND_OUTPUT, value, 1}, + {"take", NULL, PURE_KIND_MESSAGE, value, 1}, + {"answer", NULL, PURE_KIND_MESSAGE, value, 1}}; static const O code[] = { {L, 0, 0}, {LA, 1, 0}, /* add: rows.append(row { n }) */ {XF, 1, 0}, {LR, 1, 0}, /* drop: rows.remove(rows.first) */ @@ -5484,8 +5241,8 @@ static void output_meaning(void) { static const S nums[] = {{"n", 0}}; static const F was_fields[] = {{"debit", PURE_FIELD_NUMBER}, {"credit", PURE_FIELD_NUMBER}}; static const F now_fields[] = {{"credit", PURE_FIELD_NUMBER}, {"debit", PURE_FIELD_NUMBER}}; - static const I was_inputs[] = {KEY("tap", "KeyT"), {"pay", NULL, PURE_KIND_OUTPUT, false, was_fields, 2}}; - static const I now_inputs[] = {KEY("tap", "KeyT"), {"pay", NULL, PURE_KIND_OUTPUT, false, now_fields, 2}}; + static const I was_inputs[] = {KEY("tap", "KeyT"), {"pay", NULL, PURE_KIND_OUTPUT, was_fields, 2}}; + static const I now_inputs[] = {KEY("tap", "KeyT"), {"pay", NULL, PURE_KIND_OUTPUT, now_fields, 2}}; static const I none_inputs[] = {KEY("tap", "KeyT")}; static const O was_code[] = {{C, 0, 10}, {C, 0, 20}, {SEND, 1, 0}}; /* pay { debit: 10, credit: 20 } */ static const O now_code[] = {{C, 0, 20}, {C, 0, 10}, {SEND, 1, 0}}; /* the same, fields in the new order */ @@ -5543,8 +5300,8 @@ static const T bool_states[] = {{"on", 5, 1}, {"off", 5, 0}}; static const F set_fields[] = {{"on", PURE_FIELD_BOOL}, {"n", PURE_FIELD_NUMBER}}; static const F told_fields[] = {{"on", PURE_FIELD_BOOL}, {"big", PURE_FIELD_BOOL}, {"n", PURE_FIELD_NUMBER}}; static const I bool_inputs[] = {KEY("flip", "KeyF"), KEY("count", "KeyC"), KEY("check", "KeyK"), KEY("tell", "KeyT"), - {"set", NULL, PURE_KIND_MESSAGE, false, set_fields, 2}, - {"told", NULL, PURE_KIND_OUTPUT, false, told_fields, 3}}; + {"set", NULL, PURE_KIND_MESSAGE, set_fields, 2}, + {"told", NULL, PURE_KIND_OUTPUT, told_fields, 3}}; static const O bool_code[] = { {L, B_ON, 0}, {NOT, 0, 0}, {ST, B_ON, 0}, /* 0 flip: on = !on */ {L, B_ON, 0}, {JZ, 9, 0}, {L, B_N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, B_N, 0}, /* 3 count: if on { n += 1 } */ @@ -5583,12 +5340,12 @@ static void bools(void) { unsigned char payload[9] = {1}; double five = 5; memcpy(payload + 1, &five, 8); - CHECK(pure_app_message(&app, 4, NULL, -1, (PureSpan){payload, 9}) == PURE_OK && app.values[B_ON].num == 1 && app.values[B_N].num == 5); + CHECK(pure_app_message(&app, 4, (PureSpan){payload, 9}) == PURE_OK && app.values[B_ON].num == 1 && app.values[B_N].num == 5); pure_app_finish(&app); payload[0] = 2; five = 6; memcpy(payload + 1, &five, 8); - CHECK(pure_app_message(&app, 4, NULL, -1, (PureSpan){payload, 9}) == PURE_LIMIT && app.values[B_N].num == 5); + CHECK(pure_app_message(&app, 4, (PureSpan){payload, 9}) == PURE_LIMIT && app.values[B_N].num == 5); pure_app_finish(&app); /* A send holds its bools as bytes. */ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK); @@ -5800,8 +5557,8 @@ static const R item_i64_records[] = {{"Item", item_i64_fields, 2}}; static const F put_i64_fields[] = {{"id", PURE_FIELD_I64}, {"qty", PURE_FIELD_NUMBER}}; static const F echo_i64_fields[] = {{"id", PURE_FIELD_I64}}; static const I keyed_i64_inputs[] = {KEY("add", "KeyA"), KEY("low", "KeyL"), KEY("drop", "KeyD"), KEY("ask", "KeyK"), - {"put", NULL, PURE_KIND_MESSAGE, false, put_i64_fields, 2}, - {"echo", NULL, PURE_KIND_OUTPUT, false, echo_i64_fields, 1}}; + {"put", NULL, PURE_KIND_MESSAGE, put_i64_fields, 2}, + {"echo", NULL, PURE_KIND_OUTPUT, echo_i64_fields, 1}}; static Spec keyed_i64_spec(O *code) { O made[] = { @@ -5928,7 +5685,7 @@ static void integers(void) { double qty = 2.5; memcpy(payload, &least, 8); memcpy(payload + 8, &qty, 8); - CHECK(pure_app_message(&app, 4, NULL, -1, (PureSpan){payload, 16}) == PURE_OK && i64_view(&app, 2) == INT64_MIN); + CHECK(pure_app_message(&app, 4, (PureSpan){payload, 16}) == PURE_OK && i64_view(&app, 2) == INT64_MIN); pure_app_finish(&app); Outbox o = outbox_of(&app); CHECK(o.count == 1 && o.items[0].size == 8 && !memcmp(o.items[0].payload, &least, 8)); @@ -6228,7 +5985,7 @@ static void formats_and_held(void) { static const RF byte_fields[] = {{"v", 3, 0}}; static const R records[] = {{"Byte", byte_fields, 1}}; static const char *const constants[] = {"Az\xc3\xa9", ""}; - static const I inputs[] = {KEY("go", "KeyG"), KEY("grow", "KeyH"), {"ask", NULL, PURE_KIND_MESSAGE, false, NULL, 0}}; + static const I inputs[] = {KEY("go", "KeyG"), KEY("grow", "KeyH"), {"ask", NULL, PURE_KIND_MESSAGE, NULL, 0}}; static const O code[] = { /* 0 go: held5 = held5 + bytes(note); said = text(held5) */ {HLD, 5, 0}, {TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {CAT, 1, 0}, {HST, 5, 0}, {HLD, 5, 0}, {PURE_OP_TEXT_FROM, 0, 0}, @@ -6250,7 +6007,7 @@ static void formats_and_held(void) { CHECK(!strcmp(state_text(&app, 4), "Az\xc3\xa9")); /* held5 began empty again */ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_BUDGET && app.fault == PURE_FAULT_MEMORY); CHECK(!strcmp(state_text(&app, 4), "Az\xc3\xa9")); /* nothing changed */ - CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){NULL, 0}) == PURE_OK); + CHECK(pure_app_message(&app, 2, (PureSpan){NULL, 0}) == PURE_OK); Text replied = {{0}, 0}; CHECK(pure_app_reply(&app, text_span, &replied) == PURE_OK && !strcmp(replied.text, "Az\xc3\xa9")); pure_app_stop(&app); @@ -6648,7 +6405,7 @@ static void nested_rows(void) { static const T others[] = {{"order.nodes", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, {"let.kids", 3 | (uint32_t)(PURE_LIST_ROWS | PURE_LIST_BOUND) << 8, 0}, {"total", PURE_STATE_I64, 0}}; static const F fields[] = {{"nodes", PURE_FIELD_ROWS | NODES << 8}}; - static const I inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, fields, 1}}; + static const I inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, fields, 1}}; static const O code[] = { /* 0: for x in order.nodes { total += sum(k.v for k in x.kids) } */ {EACH, WALKING(NODES, 10), 0}, {ROW, NODES, 0}, {IG, ITEM(NODES, 1), 0}, {BIND_, KIDS, 0}, @@ -6669,7 +6426,7 @@ static void nested_rows(void) { size += put_node(payload + size, 2, second, 1); size += put_node(payload + size, 3, NULL, 0); for (int round = 1; round <= 3; ++round) { /* each event begins with its lists and theirs empty */ - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, size}) == PURE_OK); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, size}) == PURE_OK); CHECK(app.values[TOTAL].i64 == 21 * round && app.values[0].num == (round == 1 ? 3 : round == 2 ? 33 : 333)); CHECK(pure_list_count(app.lists[NODES]) == 0 && app.nested.count == 2); /* the third node's kids are none */ } @@ -6678,11 +6435,11 @@ static void nested_rows(void) { unsigned char broken[64]; memcpy(broken, payload, size); put32(broken + 8, 1000); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){broken, size}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); + CHECK(pure_app_message(&app, 0, (PureSpan){broken, size}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); for (size_t k = 0; k < LEN(cuts); ++k) - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, cuts[k]}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, cuts[k]}) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); CHECK(app.values[TOTAL].i64 == 63 && app.values[0].num == 333 && pure_list_count(app.lists[NODES]) == 0 && !app.nested.count); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){payload, size}) == PURE_OK && app.values[TOTAL].i64 == 84); + CHECK(pure_app_message(&app, 0, (PureSpan){payload, size}) == PURE_OK && app.values[TOTAL].i64 == 84); pure_app_stop(&app); printf("{\"nested_rows\":{\"rounds\":3,\"refused\":%zu}}\n", LEN(cuts) + 1); } @@ -6699,8 +6456,8 @@ static void bytes_fields(void) { static const R before[] = {{"Item", text_fields, 1}}, after[] = {{"Item", bytes_fields_, 2}}; static const T others[] = {{"items", 3, 0}, {"blob", PURE_STATE_TEXT, 0}}; static const F text_in[] = {{"t", PURE_FIELD_TEXT}}, bytes_in[] = {{"b", PURE_FIELD_BYTES}}; - static const I inputs_a[] = {{"put", NULL, PURE_KIND_MESSAGE, false, text_in, 1}}; - static const I inputs_b[] = {{"put", NULL, PURE_KIND_MESSAGE, false, bytes_in, 1}}; + static const I inputs_a[] = {{"put", NULL, PURE_KIND_MESSAGE, text_in, 1}}; + static const I inputs_b[] = {{"put", NULL, PURE_KIND_MESSAGE, bytes_in, 1}}; /* A: items.append(Item { t: put.t }); n = items.count */ static const O code_a[] = {{FIELD, 0, 0}, {LA, ITEMS, 0}, {LC, ITEMS, 0}, {ST, 0, 0}}; /* B: items.append(Item { t: put.b, b: bytes("zz") + put.b }); n = items.last.b's length */ @@ -6714,12 +6471,12 @@ static void bytes_fields(void) { if (load(a, &module_a) != PURE_OK || load(b, &module_b) != PURE_OK) { fprintf(stderr, "bytes fields: %s\n", error); abort(); } PureApp app; CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK); - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){(const unsigned char *)"héllo", 6}) == PURE_OK); + CHECK(pure_app_message(&app, 0, (PureSpan){(const unsigned char *)"héllo", 6}) == PURE_OK); CHECK(pure_app_replace(&app, &module_b) == PURE_OK); PureCell t = pure_list_get(app.lists[ITEMS], 0, 0), zz = pure_list_get(app.lists[ITEMS], 0, 1); CHECK(t.size == 6 && !memcmp(t.text, "héllo", 6) && zz.size == 2 && !memcmp(zz.text, "zz", 2)); static const unsigned char raw[] = {0xFF, 0x00, 0xC3, 0x28, 0x80}; /* no UTF-8 */ - CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){raw, sizeof(raw)}) == PURE_OK && app.values[0].num == 2 + sizeof(raw)); + CHECK(pure_app_message(&app, 0, (PureSpan){raw, sizeof(raw)}) == PURE_OK && app.values[0].num == 2 + sizeof(raw)); PureCell back = pure_list_get(app.lists[ITEMS], 1, 0), joined = pure_list_get(app.lists[ITEMS], 1, 1); CHECK(back.size == sizeof(raw) && !memcmp(back.text, raw, sizeof(raw)) && joined.size == 2 + sizeof(raw) && !memcmp(joined.text, "zz", 2) && !memcmp(joined.text + 2, raw, sizeof(raw))); @@ -7133,6 +6890,6 @@ int main(void) { stored_states(); deferred(); printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp)); - puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-bools-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-deferred-bursts-pointing-marks-folds-filters-keyed-lists-key-sets-arrays-text-order-message-fields-timers-now-snapshots-links\"}"); + puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-bools-replace-by-name-rejections-mutations-messages-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-key-sets-arrays-text-order-message-fields-timers-now-snapshots-links\"}"); return 0; } diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 21210c6..9d5ad1f 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -1,4 +1,4 @@ -# Pure 모듈 형식 v6 +# Pure 모듈 형식 v7 2026-09-25. 구현 `core/module.{h,c}`, `module-text.c`, `module-list.c`, 상태 저장 `module-save.c`, 모듈 파일 읽기 `module-store.c`, 트리 저장소 `store/store-tree.c`, HTTP 호스트 `serve/`. @@ -22,11 +22,16 @@ v6은 **bool**을 수와 다른 스택 종류로 더한다([016](016-bool-and-i6 v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): 새 명령, 새 타입 번호, 0이던 자리의 값(CONST와 NUM_TEXT의 인자 2, 집계의 PURE_FOLD_INTEGER)이라 판은 그대로다. 그 뒤 [027](027-computation-core.md)의 1단계로 **비트 연산, 자리의 바이트, i64 리스트의 바이트, 자리로 찾은 항목**이 더해졌다(새 명령 -넷, 90–93). 2a로 **바이트 자르기, 한 바이트, 이름으로 담은 바이트와 text**(94–97), CONCAT의 바이트, NUM_TEXT의 형식이 더해졌다. 2b로 -**레코드 안의 리스트**(레코드 필드의 타입 4, 98–100), 레지스터(리스트 상태의 플래그 512), 경로 이름의 레코드 필드, **수 읽기**(101)가 -더해졌다. 2c로 **함수**(CALL·RETURN, 102–103, 머리말의 함수 수와 함수 표)와 **글자**(CHAR_AT·CHAR, 104–105)가 더해졌다. 판은 +넷, v7의 87–90). 2a로 **바이트 자르기, 한 바이트, 이름으로 담은 바이트와 text**(91–94), CONCAT의 바이트, NUM_TEXT의 형식이 +더해졌다. 2b로 **레코드 안의 리스트**(레코드 필드의 타입 4, 95–97), 레지스터(리스트 상태의 플래그 512), 경로 이름의 레코드 필드, +**수 읽기**(98)가 더해졌다. 2c로 **함수**(CALL·RETURN, 99–100, 머리말의 함수 수와 함수 표)와 **글자**(CHAR_AT·CHAR, 101–102)가 +더해졌다. 판은 그대로다(0이던 머리말 자리에 함수 수, 함수가 없는 모듈은 배치가 같다). 3단계로 **저장과 transient 리스트의 레코드도 리스트를 담는다** (아래 "레코드 안의 리스트"). 모듈의 형식은 바뀌지 않고, 리스트의 이미지 머리가 레코드 그래프를 적는다. +v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를 없앴다: 컬렉션 표, DOC·DOC_SIZE·REPLACE, 키 있는 메시지(입력 +항목의 바이트 5는 이제 0)와 기준 리비전. 키 있는 리스트가 그 일을 한다(바이트를 담는 레코드 필드, 앱이 가진 리비전). 머리가 +38바이트로 줄었고(컬렉션 수가 빠졌다), LEN부터의 명령 번호가 당겨졌다(아래 표). 4단계가 끝날 때까지 v7은 걸음마다 바뀐다(1.0 +전, 생산자는 aup 하나다). **레코드 안의 리스트**(027의 2b). - 레코드 필드의 타입 4(LIST)는 항목들의 레코드를 기본값 자리(i64)에 둔다. @@ -55,7 +60,7 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): 내며 끝난다(스택의 나머지는 버린다). 결과는 부르는 쪽 스택에서 인자의 자리를 갖는다. - 부름마다 제 것이 있다. 스택과 지역 칸(0에서 시작)이 그렇고, 함수의 코드가 이름을 대는 것도 그렇다. 이름으로 담은 값(비어 시작), 이벤트의 리스트(비어 시작), 레지스터(아무것에도 묶이지 않고 시작)다. 부르는 쪽의 것은 부름이 돌아올 때 그대로다. -- 함수의 코드는 메시지가 없는 핸들러의 코드다(필드, 문서, 응답이 없다). 상태를 바꿀 수 있다. CALL은 핸들러와 함수의 코드에만 +- 함수의 코드는 메시지가 없는 핸들러의 코드다(필드와 응답이 없다). 상태를 바꿀 수 있다. CALL은 핸들러와 함수의 코드에만 있다(보기에는 없다). - 부름의 깊이는 이벤트의 몫이 묶는다. 부름마다 그 프레임의 바이트를 몫에 세고(넘으면 `memory`), 블록 길이를 실행량에 센다. 프레임의 바이트는 구현의 크기가 아닌 표다(`PURE_CALL_*`). 프레임 256, 스택 높이의 값마다 64, 지역 칸마다 8, 반복의 겹마다 128, @@ -68,13 +73,11 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): | 위치 | 내용 | |---|---| -| 0 | `PUREMOD6` (8 B) | +| 0 | `PUREMOD7` (8 B) | | 8 | u16 상태, u16 입력, u16 보기, u16 핸들러 | | 16 | u32 명령 수, u32 문자열 바이트 수 | -| 24 | u16 필드, u16 컬렉션, u16 상수, u16 레코드 | -| 32 | u32 상수 블롭 바이트 수, u16 레코드 필드, u16 함수 | -| 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(키별) | +| 24 | u16 필드, u16 상수, u16 레코드, u32 상수 블롭 바이트 수, u16 레코드 필드, u16 함수 | +| 38 | 상태 × 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 대상 리스트 상태 | | | 입력 × 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 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) | | | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) | @@ -109,80 +112,77 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계): | 21 | JZ | bool → | 거짓이면 `인자`로 | | 22 | JMP | | `인자`로 | | 23 | FIELD | → n, bool, b 또는 t | 메시지 필드 `인자`(u32·수는 n, bool은 bool, 바이트는 b, text는 t). 메시지 핸들러와 그 안의 집계, 그리고 인자를 받는 보기(그 표의 열과 필터, 그 안의 집계)에서 인자를 읽는다. 도착할 때 길이가 페이로드를 넘거나 bool의 바이트가 0이나 1이 아니거나 text가 UTF-8이 아니거나 수가 유한하지 않으면 메시지가 `limit`·`encoding`·`non_finite`로 실패한다 | -| 24 | DOC | → b | 컬렉션 `인자`의 메시지 키 문서. 없으면 `not_found`. 키별 메시지 핸들러 전용 | -| 25 | DOC_SIZE | → n | 그 문서의 바이트 수(없으면 0) | -| 26 | LEN | b → n | 바이트 값의 길이 | -| 27 | REPLACE | n n b → | 문서의 `[a, b)`를 값으로 바꾼다. 정수가 아니거나 범위 밖이면 `limit` | -| 28 | REPLY | b → | 이 메시지의 응답 | -| 29 | REQUIRE | bool → | 거짓이면 이벤트가 `limit`으로 실패 | -| 30 | TEXT_CONST | → t | 상수 `인자` | -| 31 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) | -| 32 | TEXT_INPUT | → t | text 입력의 내용. text 입력 핸들러 전용 | -| 33 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0`. `인자`의 비트 0–1이 1이면 bool → t: `true`·`false`, 2면 i64. 그 위는 형식([027](027-computation-core.md)): 비트 8–15 폭(왼쪽을 채움), 16 부호 뒤를 0으로 채움, 17–19 꼴(PureFormat: 10진, 16진, 대문자 16진, 2진, 8진, 고정 소수), 20–24 고정 소수의 자릿수 + 1. 정수의 꼴에 정수가 아닌 수는 `limit` | -| 34 | CONCAT | t t → t 또는 b b → b | `인자` 1이면 바이트끼리 | -| 35–37 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 | -| 38 | CUR_LOAD | → c | 커서 상태 `인자` | -| 39–40 | CUR_START CUR_END | → c | text 상태 `인자`의 처음·끝 | -| 41 | CUR_MOVE | c n → c | 단위 `인자`(0 글자, 1 줄)로 n만큼. 끝에서 멈춘다. 줄 이동은 열을 유지 | -| 42 | CUR_STORE | c → | 커서 상태 `인자`. 핸들러 전용 | -| 43 | TEXT_INSERT | c t → | text `인자`의 커서 위치에 넣는다 | -| 44 | TEXT_REMOVE | c c → | 두 커서 사이를 지운다 | -| 45 | TEXT_LINE | c → t | 커서가 있는 줄(줄바꿈 제외) | -| 46 | TEXT_SET | t → | text 전체를 바꾸고 그 text의 커서를 처음으로 | -| 47 | TEXT_FROM | b → t | UTF-8이 아니면 이벤트가 실패(encoding) | -| 48 | BYTES_FROM | t → b | 같은 바이트 | -| 49 | CUR_COUNT | c → n | 단위 `인자`로 커서 앞의 글자 수, 또는 1부터 센 줄 번호 | -| 50 | LIST_COUNT | → n | 리스트 상태 `인자`의 항목 수 | -| 51 | LIST_APPEND | f… → | 레코드 필드 값을 필드 순서대로 받아 끝에 붙인다. 키 있는 리스트에서는 키 순서의 자리에 넣고, 그 키가 이미 있으면 `exists`. 핸들러 전용 | -| 52 | LIST_INSERT | i f… → | 인덱스의 항목 앞에 넣는다(빈 리스트면 처음). 키 있는 리스트에는 쓸 수 없다 | -| 53 | LIST_REMOVE | i → | 인덱스의 항목을 지운다. 없으면 `not_found` | -| 54 | ITEM_GET | i → f | 항목의 필드. `인자`의 아래 16비트는 리스트 상태, 위는 필드 번호. 없으면 `not_found` | -| 55 | ITEM_SET | i f → | 항목의 필드를 바꾼다. 키 있는 리스트의 키 필드는 바꿀 수 없다. 핸들러 전용 | -| 56 | INDEX_LOAD | → i | 인덱스 상태 `인자` | -| 57 | INDEX_STORE | i → | 인덱스 상태 `인자`. 핸들러 전용. 같은 이벤트에서 지우기 전에 얻은, 끝을 넘은 인덱스는 `not_found` | -| 58 | INDEX_MOVE | i n → i | n만큼. 정수가 아니면 `limit`. 처음과 마지막 항목에서 멈춘다 | -| 59–60 | INDEX_FIRST INDEX_LAST | → i | 리스트 상태 `인자`의 첫·마지막 항목(빈 리스트면 0) | -| 61 | INDEX_NUM | i → n | 0부터 센 위치 | -| 62 | ROW | → i | 열이 계산하는 행. `인자`는 그 표의 리스트 상태. 열 전용 | -| 63 | PICK | … → … v | `인자`개 아래의 값을 복사해 올린다(0이면 맨 위). 타입과 대상도 그대로 | -| 64 | SLIDE | … v → v | 맨 위 값 아래의 `인자`개(1 이상)를 버린다 | -| 65 | FOLD | → n 또는 i | 집계. `인자`의 비트 0–7은 종류(0 count, 1 sum, 2 max, 3 min, 4 first, 5 distinct: 포함된 항목의 서로 다른 값의 수, −0은 0), 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 항목마다 그 리스트의 열처럼 돈다(`ROW`가 항목). count는 블록이 참을 남긴 항목을 세고, sum·max·min·distinct는 블록이 남긴 수와 포함 여부(bool)로 계산한다. first는 블록이 참을 남긴 첫 항목의 인덱스(그 리스트의 인덱스)이고 거기서 멈춘다. 포함된 항목이 없으면 count·sum은 0, max·min·first는 `not_found` | -| 66 | TEXT_COMPARE | t t → bool | 바이트 순서로 비교한다. `인자`는 관계(0 LT, 1 LE, 2 GT, 3 GE, 4 EQ, 5 NE, 수 비교 명령의 순서). 앞이 같으면 짧은 쪽이 앞이다 | -| 67 | LIST_FIND | k → i | 키 있는 리스트 `인자`에서 키가 k인 항목의 인덱스. 반씩 나눠 찾는다. 없으면 `not_found` | -| 68 | LIST_HAS | k → bool | 그 키의 항목이 있으면 참 | -| 69 | NOW | → n | 이벤트가 보는 지금(1970년부터 ms, UTC). 호스트가 정하고 스크립트는 아무 때나 재현한다. 핸들러 전용: 보기는 상태의 순수 함수로 남는다 | -| 70 | FLOOR | n → n | 내림 | -| 71 | MOD | n n → n | 나눗셈을 내림한 나머지. 결과는 나누는 수의 부호를 따른다(0도). 0으로 나누면 `divide` | -| 72 | SEND | f… → | 출력 `인자`의 필드 값을 필드 순서대로 받아(u32·수는 n, bool은 bool, 바이트는 b, text는 t) 메시지 페이로드(접두부와 가변 필드)로 만들고 이벤트의 일부로 보낸다. u32 필드의 값이 0–2³²−1의 정수가 아니면 `limit`. 핸들러 전용 | -| 73 | OUTER_ROW | → i | 열 안의 집계 블록에서 그 열의 행(집계의 항목은 ROW). 걷기 안의 집계에서는 걷는 항목 | -| 74 | EACH | | 걷기. `인자`의 비트 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 리스트가 시작할 때 가진 항목마다 차례로 핸들러의 코드로 돈다(`ROW`가 항목). 블록은 상태, 그 리스트의 항목(필드 바꾸기, 지우기), 다른 리스트를 바꿀 수 있지만 걷는 리스트에 넣을 수는 없다. 걷는 항목이나 그 앞의 항목을 지우면 걷기가 따라가 다음 항목을 건너뛰지 않는다. 걷기와 LOOP는 서로 안에 8겹까지 둔다. 안쪽의 코드는 바깥 걷기가 도는 리스트에 넣거나 지우지 못한다. 핸들러 전용 | -| 75 | LOCAL_LOAD | → n, bool 또는 i | 이벤트의 지역 칸. `인자`의 비트 0–7은 칸(64개), 8–15는 담는 것(0 수, 1 인덱스, 2 bool), 16–31은 인덱스의 리스트. 칸은 이벤트가 시작할 때 0이고 저장되지 않는다. 핸들러와 그 안의 걷기, 그 안의 집계에서 읽는다 | -| 76 | LOCAL_STORE | n, bool, i64 또는 i → | 그 칸에 넣는다. 핸들러와 그 안의 걷기에서. 담는 것 3은 i64. 한 핸들러는 한 칸을 한 가지(수, bool, i64, 또는 한 리스트의 인덱스)로만 쓴다 | -| 77–79 | I64_ADD I64_SUB I64_MUL | i64 i64 → i64 | 범위를 넘으면 `overflow` | -| 80–81 | I64_DIV I64_MOD | i64 i64 → i64 | 내림한 몫과 나머지(나머지는 나누는 수의 부호). 0으로 나누면 `divide`, 가장 작은 i64 / −1은 `overflow` | -| 82 | I64_NEG | i64 → i64 | 가장 작은 i64는 `overflow` | -| 83 | I64_COMPARE | i64 i64 → bool | `인자`는 관계(TEXT_COMPARE와 같다) | -| 84–85 | I64_MIN I64_MAX | i64 i64 → i64 | | -| 86 | I64_FROM | n → i64 | 범위 안의 정수만. 아니면 `limit` | -| 87 | NUM_FROM | i64 → n | \|i\| ≤ 2^53일 때만(정확하다). 아니면 `limit` | -| 88 | LOOP | n n 또는 i i → | 세는 반복([018](018-lifetimes-and-computation.md)). `인자`의 비트 0–7은 지역 칸, 비트 8은 i64의 반복, 16–31은 뒤따르는 블록의 길이 L. 낮음과 높음을 꺼내 그 사이의 값마다(낮음부터 높음 앞까지 1씩) 칸에 값을 두고 L개 명령을 돈다. 수는 2^53 안의 정수여야 하고 아니면 `limit`이다. 블록은 둘레 블록의 코드로 돈다(걷기 안이면 `ROW`도 그대로). 실행량은 돌 때마다 L + 1을 센다. 핸들러 전용 | -| 89 | BREAK | | 그것이 든 가장 안쪽의 LOOP나 EACH를 끝낸다(그 블록 안, 조건 안에서도). 스택이 빈 자리에서만. 핸들러 전용 | -| 90 | I64_BITS | i64 i64 → i64 | 끝없는 2의 보수 위의 정수 연산([027](027-computation-core.md)). `인자` 0 and, 1 or, 2 xor, 3 왼쪽 시프트(a·2^b, 범위를 넘으면 `overflow`), 4 오른쪽 시프트(floor(a / 2^b)). 시프트 수가 0–63 밖이면 `limit` | -| 91 | BYTE_AT | b n 또는 b i64 → i64 | 바이트 값의 그 자리 바이트(0–255). `인자` 0이면 자리가 수, 1이면 i64. 정수가 아니거나 값 밖이면 `limit` | -| 92 | BYTES_OF | → b | 리스트 상태 `인자`(레코드가 i64 필드 하나)의 항목들을 한 바이트씩. 0–255 밖의 값은 `limit` | -| 93 | INDEX_AT | n → i | 리스트 상태 `인자`의 그 자리 항목의 인덱스. 정수가 아니면 `limit`, 그 자리에 항목이 없으면 `not_found` | -| 94 | BYTES_SLICE | b n n 또는 b i64 i64 → b | a부터 c 앞까지의 바이트. `인자` 1이면 자리가 i64. 둘 다 정수이고 0 ≤ a ≤ c ≤ 길이가 아니면 `limit` | -| 95 | BYTE | i64 → b | 한 바이트. 0–255 밖이면 `limit` | -| 96 | BYTES_LOAD | → b 또는 t | 이름으로 담은 값 `인자`(0–15 바이트, 16–31 text). 이벤트와 보기의 본문이 시작할 때 비어 있다 | -| 97 | BYTES_STORE | b 또는 t → | 이름으로 담은 값 `인자`의 비트 0–4를 그 값으로(비트 8이면 뒤에 덧붙여). 담은 바이트는 이벤트의 몫에 세고, 넘으면 `memory`. text 칸에는 text만 | -| 98 | LIST_VALUE | → l | 이벤트의 리스트 상태 `인자`의 값(항목의 복사, 빈 리스트는 0). 아래 "레코드 안의 리스트" | -| 99 | LIST_BIND | l → | 묶이는 리스트 상태 `인자`(레지스터)가 그 리스트를 읽게 한다 | -| 100 | APPEND_IN | i f… → | 리스트 상태 `인자`의 비트 0–15(이벤트의 리스트)에서 인덱스의 항목, 그 필드(비트 16–31, 리스트 필드)에 담긴 리스트에 항목을 덧붙인다. 그 리스트를 다른 값이 나눠 가지면 먼저 제 것으로 복사한다 | -| 101 | NUM_PARSE | t → n | JSON의 수로 쓴 text의 가장 가까운 수. 그런 text가 아니거나 유한하지 않으면 `limit` | -| 102 | CALL | a… → r… | 함수 `인자`를 부른다. 매개변수를 꺼내고 결과를 놓는다. 위 "함수" | -| 103 | RETURN | … r… → | 함수 `인자`(자신)를 끝낸다. 맨 위의 결과가 부른 쪽으로 간다 | -| 104 | CHAR_AT | t q → q | text의 바이트 자리 q(i64)에서 시작하는 글자의 코드 포인트(i64). 글자가 시작하지 않는 자리나 밖이면 `limit` | -| 105 | CHAR | q → t | 코드 포인트 하나의 text. 0–0x10FFFF 밖이면 `limit`, 서로게이트면 `encoding` | +| 24 | LEN | b → n | 바이트 값의 길이 | +| 25 | REPLY | b → | 이 메시지의 응답 | +| 26 | REQUIRE | bool → | 거짓이면 이벤트가 `limit`으로 실패 | +| 27 | TEXT_CONST | → t | 상수 `인자` | +| 28 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) | +| 29 | TEXT_INPUT | → t | text 입력의 내용. text 입력 핸들러 전용 | +| 30 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0`. `인자`의 비트 0–1이 1이면 bool → t: `true`·`false`, 2면 i64. 그 위는 형식([027](027-computation-core.md)): 비트 8–15 폭(왼쪽을 채움), 16 부호 뒤를 0으로 채움, 17–19 꼴(PureFormat: 10진, 16진, 대문자 16진, 2진, 8진, 고정 소수), 20–24 고정 소수의 자릿수 + 1. 정수의 꼴에 정수가 아닌 수는 `limit` | +| 31 | CONCAT | t t → t 또는 b b → b | `인자` 1이면 바이트끼리 | +| 32–34 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 | +| 35 | CUR_LOAD | → c | 커서 상태 `인자` | +| 36–37 | CUR_START CUR_END | → c | text 상태 `인자`의 처음·끝 | +| 38 | CUR_MOVE | c n → c | 단위 `인자`(0 글자, 1 줄)로 n만큼. 끝에서 멈춘다. 줄 이동은 열을 유지 | +| 39 | CUR_STORE | c → | 커서 상태 `인자`. 핸들러 전용 | +| 40 | TEXT_INSERT | c t → | text `인자`의 커서 위치에 넣는다 | +| 41 | TEXT_REMOVE | c c → | 두 커서 사이를 지운다 | +| 42 | TEXT_LINE | c → t | 커서가 있는 줄(줄바꿈 제외) | +| 43 | TEXT_SET | t → | text 전체를 바꾸고 그 text의 커서를 처음으로 | +| 44 | TEXT_FROM | b → t | UTF-8이 아니면 이벤트가 실패(encoding) | +| 45 | BYTES_FROM | t → b | 같은 바이트 | +| 46 | CUR_COUNT | c → n | 단위 `인자`로 커서 앞의 글자 수, 또는 1부터 센 줄 번호 | +| 47 | LIST_COUNT | → n | 리스트 상태 `인자`의 항목 수 | +| 48 | LIST_APPEND | f… → | 레코드 필드 값을 필드 순서대로 받아 끝에 붙인다. 키 있는 리스트에서는 키 순서의 자리에 넣고, 그 키가 이미 있으면 `exists`. 핸들러 전용 | +| 49 | LIST_INSERT | i f… → | 인덱스의 항목 앞에 넣는다(빈 리스트면 처음). 키 있는 리스트에는 쓸 수 없다 | +| 50 | LIST_REMOVE | i → | 인덱스의 항목을 지운다. 없으면 `not_found` | +| 51 | ITEM_GET | i → f | 항목의 필드. `인자`의 아래 16비트는 리스트 상태, 위는 필드 번호. 없으면 `not_found` | +| 52 | ITEM_SET | i f → | 항목의 필드를 바꾼다. 키 있는 리스트의 키 필드는 바꿀 수 없다. 핸들러 전용 | +| 53 | INDEX_LOAD | → i | 인덱스 상태 `인자` | +| 54 | INDEX_STORE | i → | 인덱스 상태 `인자`. 핸들러 전용. 같은 이벤트에서 지우기 전에 얻은, 끝을 넘은 인덱스는 `not_found` | +| 55 | INDEX_MOVE | i n → i | n만큼. 정수가 아니면 `limit`. 처음과 마지막 항목에서 멈춘다 | +| 56–57 | INDEX_FIRST INDEX_LAST | → i | 리스트 상태 `인자`의 첫·마지막 항목(빈 리스트면 0) | +| 58 | INDEX_NUM | i → n | 0부터 센 위치 | +| 59 | ROW | → i | 열이 계산하는 행. `인자`는 그 표의 리스트 상태. 열 전용 | +| 60 | PICK | … → … v | `인자`개 아래의 값을 복사해 올린다(0이면 맨 위). 타입과 대상도 그대로 | +| 61 | SLIDE | … v → v | 맨 위 값 아래의 `인자`개(1 이상)를 버린다 | +| 62 | FOLD | → n 또는 i | 집계. `인자`의 비트 0–7은 종류(0 count, 1 sum, 2 max, 3 min, 4 first, 5 distinct: 포함된 항목의 서로 다른 값의 수, −0은 0), 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 항목마다 그 리스트의 열처럼 돈다(`ROW`가 항목). count는 블록이 참을 남긴 항목을 세고, sum·max·min·distinct는 블록이 남긴 수와 포함 여부(bool)로 계산한다. first는 블록이 참을 남긴 첫 항목의 인덱스(그 리스트의 인덱스)이고 거기서 멈춘다. 포함된 항목이 없으면 count·sum은 0, max·min·first는 `not_found` | +| 63 | TEXT_COMPARE | t t → bool | 바이트 순서로 비교한다. `인자`는 관계(0 LT, 1 LE, 2 GT, 3 GE, 4 EQ, 5 NE, 수 비교 명령의 순서). 앞이 같으면 짧은 쪽이 앞이다 | +| 64 | LIST_FIND | k → i | 키 있는 리스트 `인자`에서 키가 k인 항목의 인덱스. 반씩 나눠 찾는다. 없으면 `not_found` | +| 65 | LIST_HAS | k → bool | 그 키의 항목이 있으면 참 | +| 66 | NOW | → n | 이벤트가 보는 지금(1970년부터 ms, UTC). 호스트가 정하고 스크립트는 아무 때나 재현한다. 핸들러 전용: 보기는 상태의 순수 함수로 남는다 | +| 67 | FLOOR | n → n | 내림 | +| 68 | MOD | n n → n | 나눗셈을 내림한 나머지. 결과는 나누는 수의 부호를 따른다(0도). 0으로 나누면 `divide` | +| 69 | SEND | f… → | 출력 `인자`의 필드 값을 필드 순서대로 받아(u32·수는 n, bool은 bool, 바이트는 b, text는 t) 메시지 페이로드(접두부와 가변 필드)로 만들고 이벤트의 일부로 보낸다. u32 필드의 값이 0–2³²−1의 정수가 아니면 `limit`. 핸들러 전용 | +| 70 | OUTER_ROW | → i | 열 안의 집계 블록에서 그 열의 행(집계의 항목은 ROW). 걷기 안의 집계에서는 걷는 항목 | +| 71 | EACH | | 걷기. `인자`의 비트 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 리스트가 시작할 때 가진 항목마다 차례로 핸들러의 코드로 돈다(`ROW`가 항목). 블록은 상태, 그 리스트의 항목(필드 바꾸기, 지우기), 다른 리스트를 바꿀 수 있지만 걷는 리스트에 넣을 수는 없다. 걷는 항목이나 그 앞의 항목을 지우면 걷기가 따라가 다음 항목을 건너뛰지 않는다. 걷기와 LOOP는 서로 안에 8겹까지 둔다. 안쪽의 코드는 바깥 걷기가 도는 리스트에 넣거나 지우지 못한다. 핸들러 전용 | +| 72 | LOCAL_LOAD | → n, bool 또는 i | 이벤트의 지역 칸. `인자`의 비트 0–7은 칸(64개), 8–15는 담는 것(0 수, 1 인덱스, 2 bool), 16–31은 인덱스의 리스트. 칸은 이벤트가 시작할 때 0이고 저장되지 않는다. 핸들러와 그 안의 걷기, 그 안의 집계에서 읽는다 | +| 73 | LOCAL_STORE | n, bool, i64 또는 i → | 그 칸에 넣는다. 핸들러와 그 안의 걷기에서. 담는 것 3은 i64. 한 핸들러는 한 칸을 한 가지(수, bool, i64, 또는 한 리스트의 인덱스)로만 쓴다 | +| 74–76 | I64_ADD I64_SUB I64_MUL | i64 i64 → i64 | 범위를 넘으면 `overflow` | +| 77–78 | I64_DIV I64_MOD | i64 i64 → i64 | 내림한 몫과 나머지(나머지는 나누는 수의 부호). 0으로 나누면 `divide`, 가장 작은 i64 / −1은 `overflow` | +| 79 | I64_NEG | i64 → i64 | 가장 작은 i64는 `overflow` | +| 80 | I64_COMPARE | i64 i64 → bool | `인자`는 관계(TEXT_COMPARE와 같다) | +| 81–82 | I64_MIN I64_MAX | i64 i64 → i64 | | +| 83 | I64_FROM | n → i64 | 범위 안의 정수만. 아니면 `limit` | +| 84 | NUM_FROM | i64 → n | \|i\| ≤ 2^53일 때만(정확하다). 아니면 `limit` | +| 85 | LOOP | n n 또는 i i → | 세는 반복([018](018-lifetimes-and-computation.md)). `인자`의 비트 0–7은 지역 칸, 비트 8은 i64의 반복, 16–31은 뒤따르는 블록의 길이 L. 낮음과 높음을 꺼내 그 사이의 값마다(낮음부터 높음 앞까지 1씩) 칸에 값을 두고 L개 명령을 돈다. 수는 2^53 안의 정수여야 하고 아니면 `limit`이다. 블록은 둘레 블록의 코드로 돈다(걷기 안이면 `ROW`도 그대로). 실행량은 돌 때마다 L + 1을 센다. 핸들러 전용 | +| 86 | BREAK | | 그것이 든 가장 안쪽의 LOOP나 EACH를 끝낸다(그 블록 안, 조건 안에서도). 스택이 빈 자리에서만. 핸들러 전용 | +| 87 | I64_BITS | i64 i64 → i64 | 끝없는 2의 보수 위의 정수 연산([027](027-computation-core.md)). `인자` 0 and, 1 or, 2 xor, 3 왼쪽 시프트(a·2^b, 범위를 넘으면 `overflow`), 4 오른쪽 시프트(floor(a / 2^b)). 시프트 수가 0–63 밖이면 `limit` | +| 88 | BYTE_AT | b n 또는 b i64 → i64 | 바이트 값의 그 자리 바이트(0–255). `인자` 0이면 자리가 수, 1이면 i64. 정수가 아니거나 값 밖이면 `limit` | +| 89 | BYTES_OF | → b | 리스트 상태 `인자`(레코드가 i64 필드 하나)의 항목들을 한 바이트씩. 0–255 밖의 값은 `limit` | +| 90 | INDEX_AT | n → i | 리스트 상태 `인자`의 그 자리 항목의 인덱스. 정수가 아니면 `limit`, 그 자리에 항목이 없으면 `not_found` | +| 91 | BYTES_SLICE | b n n 또는 b i64 i64 → b | a부터 c 앞까지의 바이트. `인자` 1이면 자리가 i64. 둘 다 정수이고 0 ≤ a ≤ c ≤ 길이가 아니면 `limit` | +| 92 | BYTE | i64 → b | 한 바이트. 0–255 밖이면 `limit` | +| 93 | BYTES_LOAD | → b 또는 t | 이름으로 담은 값 `인자`(0–15 바이트, 16–31 text). 이벤트와 보기의 본문이 시작할 때 비어 있다 | +| 94 | BYTES_STORE | b 또는 t → | 이름으로 담은 값 `인자`의 비트 0–4를 그 값으로(비트 8이면 뒤에 덧붙여). 담은 바이트는 이벤트의 몫에 세고, 넘으면 `memory`. text 칸에는 text만 | +| 95 | LIST_VALUE | → l | 이벤트의 리스트 상태 `인자`의 값(항목의 복사, 빈 리스트는 0). 아래 "레코드 안의 리스트" | +| 96 | LIST_BIND | l → | 묶이는 리스트 상태 `인자`(레지스터)가 그 리스트를 읽게 한다 | +| 97 | APPEND_IN | i f… → | 리스트 상태 `인자`의 비트 0–15(이벤트의 리스트)에서 인덱스의 항목, 그 필드(비트 16–31, 리스트 필드)에 담긴 리스트에 항목을 덧붙인다. 그 리스트를 다른 값이 나눠 가지면 먼저 제 것으로 복사한다 | +| 98 | NUM_PARSE | t → n | JSON의 수로 쓴 text의 가장 가까운 수. 그런 text가 아니거나 유한하지 않으면 `limit` | +| 99 | CALL | a… → r… | 함수 `인자`를 부른다. 매개변수를 꺼내고 결과를 놓는다. 위 "함수" | +| 100 | RETURN | … r… → | 함수 `인자`(자신)를 끝낸다. 맨 위의 결과가 부른 쪽으로 간다 | +| 101 | CHAR_AT | t q → q | text의 바이트 자리 q(i64)에서 시작하는 글자의 코드 포인트(i64). 글자가 시작하지 않는 자리나 밖이면 `limit` | +| 102 | CHAR | q → t | 코드 포인트 하나의 text. 0–0x10FFFF 밖이면 `limit`, 서로게이트면 `encoding` | bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이트의 정수다. CONST의 인자 2는 i64 상수(값의 8바이트), NUM_TEXT의 인자 2는 i64의 10진 글자다. LOAD·STORE·지역 칸·항목·FIELD·SEND는 i64를 수처럼 옮긴다. FOLD의 종류에 64(PURE_FOLD_INTEGER)를 @@ -192,10 +192,10 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이� ## 로드할 때 검증하는 것 - 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(상태 32, 입력 32, 보기 64(열 포함), 핸들러 64, 명령 65,535, - text 상수 4,096개·1 MiB, 메시지 필드 64, 컬렉션 1, 이름 문자열 32 KiB)와 정확한 파일 크기. 명령과 상수는 모듈 크기에 맞춘 + text 상수 4,096개·1 MiB, 메시지 필드 64, 이름 문자열 32 KiB)와 정확한 파일 크기. 명령과 상수는 모듈 크기에 맞춘 블록 하나에 담긴다(`PureModule`의 다른 표는 고정 크기로 13 KiB). 그래서 한도를 올려도 작은 앱의 메모리는 그대로다. 호스트는 모듈 파일을 크기대로 읽는다(`pure_module_read`). -- 이름은 ASCII 식별자 1–31바이트이고, 상태·레코드·컬렉션·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지나 +- 이름은 ASCII 식별자 1–31바이트이고, 상태·레코드·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지나 레코드 안에서 유일하다. 메시지 필드의 이름은 식별자를 점으로 이은 경로일 수 있고(그룹 `user` 안의 `user.name`), 한 이름이 필드이면서 그룹일 수는 없다(`user`와 `user.name`). 레코드(최대 16)의 필드 범위는 차례로 이어지고 모두 합쳐 128개 이하다. 키 입력의 키는 정해진 목록(`Space Enter` 화살표 `Backspace Delete Home End Tab Escape PageUp PageDown KeyA–Z Digit0–9`) @@ -230,8 +230,8 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이� 가지다). BREAK는 반복 안에서, 스택이 빈 자리에서만 쓰고 블록 끝으로의 점프로 검증한다. 응답은 어느 반복 안에도 없다. - 보기의 본문(018의 3단계): 표 밖의 보기는 계산할 수 있다. 그 코드는 지역 칸, LOOP, BREAK, EACH를 쓰고 이벤트의 리스트를 바꿀 수 있지만(LIST_APPEND·LIST_INSERT·LIST_REMOVE·ITEM_SET) 다른 상태는 바꾸지 못한다(`view changes a state`). 반복의 블록은 - 값을 남기지 않는 보기의 문장으로, 걷기의 블록은 그 항목을 읽는 보기의 문장으로 검증한다. 상태 쓰기, SEND, REPLY, REQUIRE, 문서 - 연산, DT, NOW는 보기 어디에도 없다. 로드할 때 보기마다 계산하는지(지역 칸이나 반복을 쓰는지)와 바꾸는 이벤트의 리스트를 적는다. + 값을 남기지 않는 보기의 문장으로, 걷기의 블록은 그 항목을 읽는 보기의 문장으로 검증한다. 상태 쓰기, SEND, REPLY, REQUIRE, + DT, NOW는 보기 어디에도 없다. 로드할 때 보기마다 계산하는지(지역 칸이나 반복을 쓰는지)와 바꾸는 이벤트의 리스트를 적는다. - 이벤트의 리스트: 플래그 256(PURE_LIST_ROWS)을 가진 리스트는 이벤트가 끝날 때마다 비워지고 저장되지 않으며 되돌리기 기록을 쌓지 않는다. 이름은 경로다. `입력.필드`이면 그 메시지의 행 필드가 채우고(키 없이), `let.이름`이면 코드만 채운다(aup의 `let 이름: list of 레코드`, 키를 둘 수 있다: 플래그 256 + 키 필드 + 1). 저장하지 않는 상태(비트 8)일 수 없다. @@ -239,7 +239,7 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이� 경로`와 같다. 그래서 행 목록 하나는 필드 하나가 채운다. FIELD는 행 필드를 읽지 않는다(행은 리스트로 읽는다). - 인자(종류 6)는 열이 아닌 보기 하나를 가리키고, 보기마다 하나이며, 이름이 `보기 이름.args`다. 핸들러를 갖지 않는다. 그 보기와(표라면 그 열과 필터도) 그 안의 집계는 FIELD로 인자를 읽는다. 집계 블록은 둘레 블록의 메시지나 인자를 읽을 수 있다 - (읽기뿐: 응답과 문서 연산은 핸들러 자신의 코드에만 있다). + (읽기뿐: 응답은 핸들러 자신의 코드에만 있다). - 표는 리스트 상태를 가리키고 열은 앞에 나온 표를 가리킨다. 표 밖의 이름은 한 이름공간이고, 열 이름은 표 안에서만 유일하다. - 반복(LOOP, EACH)은 제 피연산자 말고는 빈 스택에서 연다. 반복의 블록은 그 실행의 스택 바닥에서 시작하므로, 아래에 값을 둔 반복은 @@ -343,11 +343,9 @@ text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨� 호스트가 앱을 돌리는 동안 타이머의 주기마다 한 번 넣고, 놓친 틱을 몰아서 넣지는 않는다. 이벤트가 보는 시각은 `PureApp.now`이고 호스트가 정한다(`pure-run`의 `time MS`, `tick NAME`). 메시지 이벤트는 다음 순서로 처리한다. -1. 입력 종류, 키 형식, 기준 리비전(`-1`은 없음)을 확인하고, 페이로드가 고정 부분보다 짧으면 `limit`. -2. 키별 메시지면 데이터 호스트에서 그 키의 작업 버전을 연다. 기준 리비전이 있고 현재와 다르면 `conflict`. -3. 핸들러를 소스 순서대로 실행한다. 실패하면 작업 버전을 버리고 상태도 되돌린다. -4. 문서를 바꿨다면 기준 리비전이 있어야 하고, 작업 버전을 커밋한다. 문서에서 읽은 응답은 커밋 전에 복사한다. - 커밋이 실패하면 상태를 되돌린다. 결과 불명(`uncertain`)이면 상태만 되돌리고 그렇게 보고한다. +1. 입력 종류를 확인하고, 페이로드가 고정 부분보다 짧으면 `limit`. 행을 행 목록에 채운다. +2. 핸들러를 소스 순서대로 실행한다. 실패하면 상태를 되돌린다. +3. 저장한다(지연 저장이면 묶음의 저장에서). 저장이 실패하면 상태를 되돌린다. 결과 불명(`uncertain`)이면 그렇게 보고한다. 모듈을 바꾸면(`pure_app_replace`) 상태는 이름과 종류가 같을 때 남는다. 수와 bool은 서로 옮긴다. 수 0(−0도)과 1은 false와 true가 되고 다른 수는 기본값이 되며, bool은 0과 1이 된다. 레코드가 같으면 리스트를 그대로 쓰고, @@ -355,8 +353,8 @@ true가 되고 다른 수는 기본값이 되며, bool은 0과 1이 된다. 레� 순서로 놓인다). 커서와 인덱스는 대상 text·리스트가 남을 때만 위치를 유지한다. -응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드와 문서의 익스텐트를 복사하지 않는다. 문서를 고친 이벤트의 -문서 응답과 text 상태의 응답은 확정 전에 복사한다. text 보기는 `pure_app_view_text`로 빌려준다. +응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드를 복사하지 않는다. text 상태의 응답은 확정 전에 복사한다. text 보기는 +`pure_app_view_text`로 빌려준다. 호스트는 인덱스 상태를 한 항목으로 옮길 수 있다(`pure_app_point`, 사용자가 행을 가리키는 것). 핸들러 없는 이벤트이며 키 이벤트처럼 저장되고, 마지막 항목을 넘으면 `not_found`다. 어느 인덱스를 옮길지는 보기가 알려 준다. @@ -371,8 +369,7 @@ text·리스트가 남을 때만 위치를 유지한다. `pure_app_cell`·`pure_app_cell_text`로 행 번호와 함께 계산한다. 화면 모델을 따로 만들거나 목록을 복사하지 않으므로, 10만 항목 목록에서 20줄을 그리는 표면은 20행만 계산한다(가상 스크롤이 런타임의 성질이 된다). 보기가 실패해도 앱의 마지막 이벤트 실패 정보는 바뀌지 않는다. -데이터 호스트는 `PureData` 인터페이스이며, `pure_tree_data`가 트리 저장소의 draft로 구현한다. -`pure-edit-module`은 기존 문서 CLI(`pure-edit`)와 같은 명령·출력·종료 코드를 모듈로 제공한다. +데이터 호스트는 `PureData` 인터페이스(상태를 열고 함께 저장한다)이며, `pure_tree_data`가 트리 저장소의 draft로 구현한다. ## 상태 영속 @@ -382,7 +379,7 @@ text·리스트가 남을 때만 위치를 유지한다. 실패한 이벤트는 그것도 되돌린다. 그것만 바꾼 이벤트의 저장은 아무것도 쓰지 않는다. 스냅숏은 transient text를 복사한다(저장소에 없으니 매핑을 나눌 수 없다). -| 상태 | 저장 형태 (런타임 이름공간, 문서 키와 겹치지 않음) | +| 상태 | 저장 형태 (런타임 이름공간) | |---|---| | text | `text-이름`: UTF-8 바이트 그대로. 앱의 draft가 곧 저장 문서라 입력 한 글자는 바뀐 익스텐트만 추가한다 | | 리스트 | 이미지와 로그(아래). 트리에는 두지 않는다 | @@ -417,7 +414,7 @@ text·리스트가 남을 때만 위치를 유지한다. 리스트 필드의 항목도 그래프의 레코드를 따라 같은 규칙으로 옮겨진다(3단계). 커서가 text 범위나 글자 경계를 벗어나거나, 인덱스가 리스트 밖이면 0이 된다. 깨진 리스트·text(형식 오류, UTF-8 아님)는 시작을 `store_error`로 멈춘다. 조용히 버리지 않는다. -- **저장은 묶음이다.** 저장 한 번이 마지막 저장 이후 메시지가 고친 문서들과 바뀐 상태 문서들을 저장소의 +- **저장은 묶음이다.** 저장 한 번이 마지막 저장 이후 바뀐 상태들을 저장소의 **한 커밋**(`pure_draft_commit_all`)과 로그 항목 하나로 공개한다. 모두 보이거나 하나도 보이지 않는다. 저장이 실패하면 마지막 저장 이후의 이벤트가 **모두** 되돌려진다(값, text draft, 리스트 되돌리기 기록과 저널, 이벤트·tick 수). 그래서 "성공을 알린 이벤트는 저장됐고, 실패를 알린 이벤트는 흔적이 없다"가 성립한다. 결과 불명(`uncertain`)이나 다른 @@ -426,8 +423,7 @@ text·리스트가 남을 때만 위치를 유지한다. frame 이벤트는 다음 이벤트나 `pure_app_save`에 함께 들어간다. `pure_app_stop`만 하면 저장하지 않은 변경은 사라진다. - **지연 저장(그룹 커밋).** `pure_app_defer(app, true)`면 이벤트는 바로 적용되고, 호스트가 입력을 몰아 처리한 뒤 `pure_app_save` 한 번으로 저장한다. 이벤트는 그 뒤 저장이 성공해야 확정이고, 그 전에는 응답을 내보내지 않는 - 것이 호스트의 몫이다. 한 묶음에서 키 하나의 문서는 한 번만 열린다. 같은 키의 두 번째 메시지나 256개 키를 넘는 - 메시지는 먼저 묶음을 저장하므로 문서 리비전은 저장마다 한 칸씩 움직인다(응답에 알린 리비전 = 저장된 리비전). + 것이 호스트의 몫이다. Pi 5의 SD(ext4)에서 이벤트 600개: 이벤트마다 저장 6,282 ms(10.47 ms/이벤트), 8개씩 973 ms, 64개씩 341 ms, 한 번에 194 ms. `pure-run --store 경로 --batch N`으로 잰다. - **교체.** 모듈을 바꾸면 새 상태는 저장된 값이 있으면 그것으로 시작하고, 버린 상태의 저장본은 그대로 남는다. @@ -445,7 +441,7 @@ text·리스트가 남을 때만 위치를 유지한다. 검증(`test-module`): 같은 무작위 이벤트열을 메모리 앱과 수시로 재시작하는 저장 앱에 주어 매 단계 상태가 비트 단위로 같다(리스트 앱·text 앱 각 400단계, 재시작 20회 이상). 수·text·리스트를 한꺼번에 바꾸는 이벤트 중 커밋의 모든 단계에서 -프로세스를 죽여도 다시 시작하면 전부 이전 아니면 전부 이후다. 메시지의 문서와 상태가 함께 커밋되고 함께 실패한다. +프로세스를 죽여도 다시 시작하면 전부 이전 아니면 전부 이후다. 메시지가 바꾼 상태가 함께 커밋되고 함께 실패한다. 레코드 변경은 재시작과 교체 모두에서 항목을 옮긴다. 지연 저장에서는 저장된 이벤트만 다시 재생한 메모리 앱과, 무작위로 저장하고 가끔 저장 없이 멈췄다 다시 시작하는 저장 앱이 매 단계 같다(리스트·text 앱 각 600단계). 묶음 저장이 실패하면 묶음 전체가 되돌려지고, 저장 중 프로세스가 죽으면 묶음 전체가 남거나 전혀 남지 않는다. 비용(Pi 5, SD의 ext4): 저장하는 이벤트 하나에 약 10.6 ms가 @@ -486,7 +482,6 @@ text·리스트가 남을 때만 위치를 유지한다. | `GET /views/이름?인자=값…` | 인자를 받는 보기(표라면 `from`·`count`와 함께). 값은 `%XX`를 풀어 읽고, 고정 필드는 메시지의 쿼리 필드와 같다. 빠진 고정 인자, 모르는 이름, 두 번 준 인자, 타입이 다른 값은 400 `invalid`. 등록한 질의도 같다(`GET /queries/이름?인자=값`) | | `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수, bool)를 쿼리(`?points=3`, `?done=true`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, `true`·`false`는 bool, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) | | `POST /inputs/이름` + `X-Pure-Source`·`X-Pure-Sequence` | 다른 앱의 출력에서 전해진 메시지(`pure_app_deliver`): 번호마다 한 번 적용하고, 이미 들은 번호는 200. 답에 `X-Pure-Batch: 256`을 싣고, `X-Pure-Count: N`이면 본문은 N개 객체의 JSON 배열(연속 번호, 한 묶음에서 차례로 적용) | -| `GET`·`PUT /objects/키` | 모듈에 키별 메시지 `show`·`edit`가 있으면 기존 문서 서버(`server.c`)와 같은 계약. `GET`은 `show`, `PUT`(`If-Match`/`If-None-Match`)은 `edit` | - **실패는 실패 종류로 답한다.** 이벤트가 실패하면 `X-Pure-Error`에 종류를 싣는다. `not_found` 404, `exists` 409, `limit`·`divide`·`clamp`·`non_finite`·`encoding` 422, UTF-8이 아닌 text 400. 값이 없는 보기는 404 `no_value`, 없는 이름은 @@ -505,8 +500,8 @@ text·리스트가 남을 때만 위치를 유지한다. (`pure_runtime_grow`: 늘린 만큼 예산에 계상, 모자라면 503 `payload_budget`) 본문 뒤에 둔다. 가변 필드가 행 필드 하나뿐인 메시지는 JSON이 아닌 본문을 그 행의 저장 형식 그대로 받는다(대량 넣기: 3만 행 JSON 한 요청이 초당 약 24만 행). `--reply`의 응답 JSON도 같은 방식으로 행을 준다(없으면 행 없음). 행이 든 메시지는 전달 묶음(`X-Pure-Count`)에 들어가지 않는다. -- **라우트는 작업 슬롯에 둔다.** 요청이 가리키는 것(문서, 보기, 입력)과 메시지 필드는 받을 때 풀어 슬롯별 표와 ingress에 - 둔다. 연결이 묶음 전에 끊겨도 받아들인 이벤트는 실행된다. 런타임 코어는 그대로다(키는 문서 키 또는 보기·입력 이름). +- **라우트는 작업 슬롯에 둔다.** 요청이 가리키는 것(보기, 입력, 질의)과 메시지 필드는 받을 때 풀어 슬롯별 표와 ingress에 + 둔다. 연결이 묶음 전에 끊겨도 받아들인 이벤트는 실행된다. 런타임 코어는 그대로다(키는 보기·입력 이름). - **시간.** 요청의 이벤트는 적용되는 순간의 벽시계를 `now`로 본다. 앱을 돌리는 스레드(한 스레드면 그 스레드, 아니면 소유자)가 상태를 가진 앱의 타이머를 제때 틱하고, 틱들을 저장 한 번으로 저장한다. 그래서 다시 띄운 서버는 저장된 상태에서 이어서 틱한다. 대기는 다음 틱까지다(epoll의 대기 시간, 소유자는 조건 변수의 시한). @@ -518,19 +513,18 @@ text·리스트가 남을 때만 위치를 유지한다. 열린 연결 수를 따른다(기본 최대 1,024). 응답은 머리말과 본문을 `writev` 한 번으로 보낸다. 연결 기한(기본 10초)은 진행이 멈춘 시간으로 센다. 느리게라도 계속 받아 가는 클라이언트는 큰 응답을 끝까지 받는다. - **런타임 코어.** 요청은 `runtime.c`를 그대로 거친다(연결마다 작업 슬롯, 입력 바이트 예산, 코어가 소유한 - ingress, 요청에서 빌린 PUT 응답). 효과는 비동기 인터페이스(`pure_runtime_take`/`complete`)로 소유 스레드에서 처리한다. + ingress, 요청에서 빌린 쓰기의 응답). 효과는 비동기 인터페이스(`pure_runtime_take`/`complete`)로 소유 스레드에서 처리한다. - **이벤트 한 바퀴가 묶음 하나다.** 준비된 요청을 모두 지연 저장 모드의 앱에 차례로 적용하고, 저장 한 번 **뒤에** - 응답한다. 저장이 실패하면 묶음의 읽기까지 모두 실패한다. 같은 문서 키가 다시 오거나 문서 수가 한 저장의 한도를 - 넘으려 하면 묶음을 먼저 끝낸다. 앱 상태의 이벤트는 어느 묶음에나 들어간다. 그래서 묶음 안의 모든 요청은 - 자기 묶음의 저장으로 저장되거나 그것과 함께 실패한다. 보기는 저장된 상태만 내보낸다(아래 스냅숏). 한 연결의 + 응답한다. 저장이 실패하면 묶음의 읽기까지 모두 실패한다. 그래서 묶음 안의 모든 요청은 자기 묶음의 저장으로 + 저장되거나 그것과 함께 실패한다. 보기는 저장된 상태만 내보낸다(아래 스냅숏). 한 연결의 요청은 앞 요청에 답한 뒤에 읽으므로 쓴 것을 곧 읽는다. -- **저장소 핸들.** 문서와 상태를 저장소 핸들(`pure_store_data`)로 열어 파일 매핑 하나를 공유하고, 묶음마다 한 번만 +- **저장소 핸들.** 상태를 저장소 핸들(`pure_store_data`)로 열어 파일 매핑 하나를 공유하고, 묶음마다 한 번만 저장소가 바뀌었는지 본다. - **연결 스레드와 소유자.** `--threads N`은 스레드마다 같은 포트의 리스너(`SO_REUSEPORT`), epoll, 런타임 코어를 둔다. `--threads auto`는 프로세스가 쓸 수 있는 CPU마다 하나를 두고, 상태를 가진 앱이면 소유자 몫으로 하나를 뺀다. - 키별 문서만 가진 모듈은 스레드마다 앱과 저장소 핸들을 두고 저장소 말고는 아무것도 나누지 않는다. 상태를 가진 + 상태도 출력도 없는 모듈은 스레드마다 앱을 두고 아무것도 나누지 않는다. 상태를 가진 모듈은 상태의 소유자가 하나여야 하므로, 연결 스레드가 하나여도 따로 소유자 스레드가 앱을 돌린다. 연결 스레드는 - 요청을 받고 해석하고 답을 쓰는 일(시스템 호출이 대부분이다)을 나눠 맡고, 입력과 문서 요청을 소유자에게 넘긴다. + 요청을 받고 해석하고 답을 쓰는 일(시스템 호출이 대부분이다)을 나눠 맡고, 입력을 소유자에게 넘긴다. 소유자는 모든 스레드의 요청을 한 묶음으로 적용하고 저장 한 번 뒤에 각 스레드에 돌려주며(eventfd로 깨운다), 스레드는 돌려받은 것을 완료하고 답한다. 작업 슬롯의 라우트와 입력은 완료될 때까지 그 스레드의 런타임 코어가 붙잡고, 빌려준 저장소 매핑의 참조 수는 원자적이다. @@ -552,15 +546,14 @@ text·리스트가 남을 때만 위치를 유지한다. 평가하고 답을 모아 보낸다. 입력은 그보다 먼저 소유자에게 넘겨, 소유자가 그동안 적용하고 저장한다. 그래서 쓰기의 답을 받은 뒤의 읽기는 그 쓰기를 보고, 저장되지 않았거나 실패로 되돌린 상태는 아무도 보지 않으며, 소유자가 동기화하는 동안에도 읽기는 기다리지 않는다. 스냅숏을 만들지 못하면 보기도 소유자가 답한다. -- 저장된 문서를 읽는 GET 응답은 복사하지 않고 저장소 매핑의 익스텐트를 빌려준다. 보내는 동안 문서가 바뀌어도 - 읽은 버전이 나간다. 빌려준 응답도 복사한 응답처럼 예산에 계상된다. 매핑 참조는 조각으로 보내는 자원으로 +- text 상태를 그대로 보이는 보기의 GET 응답은 복사하지 않고 저장소 매핑의 익스텐트를 빌려준다. 보내는 동안 text가 + 바뀌어도 읽은 버전이 나간다. 빌려준 응답도 복사한 응답처럼 예산에 계상된다. 매핑 참조는 조각으로 보내는 자원으로 런타임 코어에 넘어가, 응답이 소켓에 다 들어갈 때까지 예산을 차지하고 티켓과 함께 풀린다. 그래서 받지 않는 - 클라이언트가 붙잡을 수 있는 옛 버전은 예산을 넘지 않는다(넘으면 `503 payload_budget`). 매핑이 담지 않는 - 응답만 복사한다. PUT 응답은 요청 본문에서 빌려야 하므로 `edit` 핸들러는 메시지 필드만 응답할 수 - 있다(`reply edit.insert`). 시작할 때 확인한다. + 클라이언트가 붙잡을 수 있는 옛 버전은 예산을 넘지 않는다(넘으면 `503 payload_budget`). -성능과 비교는 [연구 075](../research/075-http-host-against-axum.md). 검증(Pure `make check`의 `tests/check.py`, 앱은 aup가 빌드): 무작위 요청에 기존 서버와 -상태 코드·ETag·오류·본문이 같고, 동시 편집이 커밋을 나누어 쓰며, `kill -9` 뒤에도 확인 응답한 편집이 모두 남는다. +성능과 비교는 [연구 075](../research/075-http-host-against-axum.md). 검증(Pure `make check`의 `tests/check.py`, 앱은 aup가 빌드): 키 있는 +리스트의 문서(`DOCS_SOURCE`)가 무작위 편집과 읽기에 모델과 상태 코드·오류·본문이 같고, 동시 편집이 커밋을 나누어 쓰며, `kill -9` +뒤에도 확인 응답한 편집이 모두 남는다. 앱 라우트는 키 있는 리스트 앱에 무작위 POST(쿼리 필드와 본문을 가진 메시지, text, 키)와 GET(수, text, 표의 쪽)을 800번 보내 상태 코드·오류·본문·JSON 행이 파이썬 모델과 같고, 멈췄다 다시 띄워도, 여섯 클라이언트가 보내는 중에 `kill -9`로 죽여도 확인 응답한 이벤트(약 4,200개)가 모두 남는다. 그동안 다른 연결의 읽기는 먼저 확인 응답된 쓰기를 @@ -596,8 +589,8 @@ text·리스트가 남을 때만 위치를 유지한다. 로더와 검증기의 실패는 한 경로에서 형식화한다. - **인터프리터.** 명령을 가져올 때 위치를 넘기고, 실패는 두 출구(명시한 실패, 데이터가 거절한 변경)로 모은다. 위치를 밀어 넣는 명령들과 이동 명령들은 한 갈래를 나눠 쓴다. -- **한 경로.** 키·text·메시지 이벤트가 같은 처리(`dispatch`)를 탄다. 메시지만의 일(기준 리비전 없는 편집 거절, - 응답 복사, 문서의 묶음 참여)은 이벤트 값으로 들어간다. 앱 시작은 "아무 모듈도 없던 앱에 모듈을 연다"이다. +- **한 경로.** 키·text·메시지 이벤트가 같은 처리(`dispatch`)를 탄다. 메시지만의 일(행, 응답 복사)은 이벤트 값으로 + 들어간다. 앱 시작은 "아무 모듈도 없던 앱에 모듈을 연다"이다. 교체와 같은 경로라, 새 상태는 저장된 값이 있으면 그것으로 시작한다(수·커서·인덱스도 이제 text·리스트와 같다). - **한 번만 도는 코드는 크기로.** 로드, 검증, 시작, 교체, 저장된 상태 열기는 `cold`로 표시해 컴파일러가 크기를 우선한다. 이벤트 경로는 그대로 속도를 우선한다. 작은 GET 처리량은 그대로다(1스레드 128k/s). diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md index 9e0ca1f..239a784 100644 --- a/docs/design/027-computation-core.md +++ b/docs/design/027-computation-core.md @@ -259,10 +259,46 @@ - 명령 수: 덧붙이기가 필드마다 타입을 두 번 묻던 것을, 로드가 만든 레코드마다의 비트 마스크(`record_held`) 하나로 바꿨다. 그래서 b32가 b30보다 적다(묶음 없는 BFS −1.05%, join −0.25%, 묶음 BFS −0.36%, 나머지 ±0.04% 안). 은행 질의(s30 대 s32)는 −0.01%에서 +0.30%다. + - **4b-2 만듦(2026-10-18).** 모듈 형식 v7. + - Pure: 컬렉션 표, DOC·DOC_SIZE·REPLACE(명령 106개 → 103개), 키 있는 메시지와 기준 리비전, PureData의 문서 콜백 여섯, + 문서별 묶음 저장(키마다 한 번, 256개까지)이 빠졌다. `pure_app_message`는 입력과 페이로드만 받는다. 머리는 38바이트다. + `pure-edit-module`도 빠졌다. + - serve: `/objects` 경로, ETag와 `If-Match`·`If-None-Match`, 412, 문서만 가진 모듈의 스레드마다 앱이 빠졌다. 상태도 출력도 + 없는 모듈만 스레드마다 돈다. + - aup: `bytes by key`, `message by key`, `이름[key]`, `.replace(..)`가 빠졌고 옛 편집기 예제 둘도 빠졌다. + - 찾아 막은 구멍: 어느 블록에도 없는 명령(돌지 않는 코드)은 검증되지 않는데, 로드의 뒤 패스들이 모든 명령의 인자를 읽었다. + 손상한 모듈의 시험이 머리가 바뀌어 다른 손상을 고르자 범위 밖을 읽고 죽었다. 이제 로드가 그런 명령을 CONST 0으로 지운다. + - 옮긴 검사: 옛 문서 서버와의 대조(`server_parity`)는 키 있는 리스트의 문서 앱(`DOCS_SOURCE`)과 파이썬 모델의 대조 + (`server_documents`, 연결 스레드 하나와 셋, 재시작)가 되었다. 파이프라이닝과 묶음 저장(kill -9)은 그 앱의 입력과 보기로, 빌려 + 읽기의 예산은 text 보기로 본다(문서의 빌려 읽기는 `server_lent_views`가 이미 text 보기로 본다). 시계 검사는 그 앱으로 편집한다. + aup의 옛 편집기 대조(`editor_parity`)는 `documents_model`이 대신한다. + - HTTP 벤치마크의 문서 묶음은 옛 문서 서버와 기준선만 돈다. + - 잃은 것: 저장소의 그룹 커밋(여러 기록자가 한 저장소에 커밋을 모으는 것)을 pure-serve가 더 쓰지 않는다. 앱마다 소유자 하나가 + 저장하기 때문이다. 그 길은 옛 문서 서버와 저장소의 시험에 남는다. + - 줄 수: 코어 22,398 → 22,204(4a와 4b-1의 더함을 넣고), serve.c 5,086 → 4,978, aup의 compiler/ 5,923 → 5,829(compile.c는 4b-1 뒤 + 5,689 → 5,587). + - 명령 수(b33 대 b32): 묶음은 BFS −0.33%, 나머지 ±0.002% 안이다. 실행기만의 길은 BFS +0.63%, knapsack +0.52%, join +0.38%다 + (`step`에서 경우 셋이 빠져 코드 생성이 움직였다). 은행 질의(s32는 v6 모듈, s33은 v7 모듈로, 각 두 번)는 field +0.68%, + found +0.44%, large +0.42%, 나머지 +0.13% 안이다. 모두 막대(1%) 안이지만 4단계 끝에 다시 센다. - **4c. 커서와 인덱스를 앵커 하나로(의심 3).** - 상태 종류 "앵커"가 text나 리스트 하나를 가리킨다. - 뜻은 그대로다: 편집을 따라가고, 글자의 경계에 서고, 지운 항목에서는 이웃으로 간다. - 같은 일을 하는 커서와 인덱스의 명령은 하나로 합친다. + - **설계(2026-10-18).** + - 둘은 같은 것이다. 열(text의 글자, 리스트의 항목) 안의 틈 하나이고, 편집을 따라 움직인다. + - 커서는 틈에 선다. 그 자리에 넣으면 뒤로 가고(마크는 남는다), 지운 범위 안이면 그 처음으로 간다. + - 인덱스는 항목 앞의 틈에 서서 그 항목을 가리킨다. 그 자리에 넣으면 뒤로 간다(항목을 따라간다). 지운 항목에서는 다음 + 항목을 가리킨다. 끝의 틈에는 서지 않는다(빈 리스트가 아니면 마지막 항목으로). + - 형식의 앵커 명령은 일곱이다. 모두 인자에 대상(text나 리스트 상태)을 적는다. + - LOAD, STORE: 앵커 상태 + - START, END: text의 처음과 끝, 리스트의 첫 항목과 마지막 항목 + - MOVE(단위): 글자, 줄, 항목으로 n만큼. 끝에서 멈춘다. + - COUNT(단위): 앵커 앞의 단위 수. 줄은 0부터 센다(aup의 `at.line`이 1을 더한다). + - AT(단위): 처음부터 n 단위 뒤의 앵커. 없으면 `not_found` + - 지금의 열셋(CUR_* 여섯, INDEX_* 일곱)이 일곱이 되고, 상태 종류가 일곱에서 여섯이 된다(bool과 i64의 번호가 당겨진다). + - 구현은 대상의 종류로 나뉜다. 로드가 형식의 명령을 대상에 맞는 내부 명령(지금의 CUR_*와 INDEX_*의 걸음)으로 바꿔 + 쓰고, 검증은 그것에 대해 지금처럼 한다. 그래서 실행기, 묶음 실행기, JIT의 길은 그대로다. 026의 원칙(뜻은 일반적으로, + 특수화는 구현 아래로)이다. - **4d. 입력은 메시지 하나(의심 1).** - 키, text, 타이머, 프레임, 가리킴은 표면과 호스트가 만드는 메시지다. - 모듈의 입력은 메시지(필드들)와 표면이 읽는 묶음(키 이름, text, 주기, 프레임)이다. 묶음을 해석하는 것은 표면이고, Pure는 메시지만 diff --git a/research/experiments/040-against-c/2026-10-18-counts-s32v6.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s32v6.jsonl new file mode 100644 index 0000000..cfef4f6 --- /dev/null +++ b/research/experiments/040-against-c/2026-10-18-counts-s32v6.jsonl @@ -0,0 +1,2 @@ +{"serve": "pure-serve", "per_branch": {"instructions": 27064595, "cycles": 11936072}, "in_credit": {"instructions": 3508659, "cycles": 1627657}, "large": {"instructions": 1423198, "cycles": 625928}, "ranks": {"instructions": 24446380, "cycles": 15093091}, "none": {"instructions": 617123, "cycles": 236726}, "field": {"instructions": 9028796, "cycles": 3384389}, "found": {"instructions": 14180082, "cycles": 12075121}} +{"serve": "pure-serve", "per_branch": {"instructions": 27033728, "cycles": 11955217}, "in_credit": {"instructions": 3501713, "cycles": 1414960}, "large": {"instructions": 1423142, "cycles": 605214}, "ranks": {"instructions": 24429139, "cycles": 14872254}, "none": {"instructions": 617115, "cycles": 192865}, "field": {"instructions": 9028788, "cycles": 3346914}, "found": {"instructions": 14179473, "cycles": 10774389}} diff --git a/research/experiments/040-against-c/2026-10-18-counts-s33.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s33.jsonl new file mode 100644 index 0000000..e1ed651 --- /dev/null +++ b/research/experiments/040-against-c/2026-10-18-counts-s33.jsonl @@ -0,0 +1,2 @@ +{"serve": "pure-serve", "per_branch": {"instructions": 26940620, "cycles": 11956474}, "in_credit": {"instructions": 3480396, "cycles": 1582124}, "large": {"instructions": 1429175, "cycles": 606440}, "ranks": {"instructions": 24391428, "cycles": 14777801}, "none": {"instructions": 617940, "cycles": 190815}, "field": {"instructions": 9090056, "cycles": 3297986}, "found": {"instructions": 14242161, "cycles": 11634241}} +{"serve": "pure-serve", "per_branch": {"instructions": 27162324, "cycles": 11942429}, "in_credit": {"instructions": 3530258, "cycles": 1604696}, "large": {"instructions": 1429143, "cycles": 613979}, "ranks": {"instructions": 24513498, "cycles": 14981857}, "none": {"instructions": 617940, "cycles": 192686}, "field": {"instructions": 9090159, "cycles": 3293663}, "found": {"instructions": 14242529, "cycles": 10703067}} diff --git a/research/experiments/040-against-c/runs.sh b/research/experiments/040-against-c/runs.sh index a265ac0..b3e95ba 100644 --- a/research/experiments/040-against-c/runs.sh +++ b/research/experiments/040-against-c/runs.sh @@ -27,7 +27,9 @@ build) git worktree add -f -q --detach "$builds/src-$2" "$3" make -s -C "$builds/src-$2" build/libpure.a mkdir -p "$builds/$2" - cc -O2 -g -std=c17 -I"$builds/src-$2/core" $here/suite-run.c "$builds/src-$2/build/libpure.a" -lm -o "$builds/$2/suite-run" + keyed="" + grep -q PURE_APP_PENDING "$builds/src-$2/core/module.h" && keyed=-DPURE_KEYED_MESSAGES # before 027's 4b + cc -O2 -g -std=c17 $keyed -I"$builds/src-$2/core" $here/suite-run.c "$builds/src-$2/build/libpure.a" -lm -o "$builds/$2/suite-run" git worktree remove --force "$builds/src-$2" for work in roots knapsack sort join words matmul; do ../aup/build/aup build $here/$work.aup -o "$builds/$2/$work.pmod"; done ../aup/build/aup build research/experiments/033-computing/graph.aup -o "$builds/$2/graph.pmod" ;; diff --git a/research/experiments/040-against-c/suite-run.c b/research/experiments/040-against-c/suite-run.c index f8c03e7..410b9dc 100644 --- a/research/experiments/040-against-c/suite-run.c +++ b/research/experiments/040-against-c/suite-run.c @@ -12,6 +12,14 @@ * Without NAMEs, all of them. PURE_JIT and PURE_AHEAD as the core reads them. */ #define _POSIX_C_SOURCE 200809L #include "module.h" +/* A message: before 027's 4b it took a key and a base revision too (runs.sh build gives -DPURE_KEYED_MESSAGES then). */ +static PureResult send_message(PureApp *app, uint32_t input, PureSpan payload) { +#ifdef PURE_KEYED_MESSAGES + return pure_app_message(app, input, NULL, -1, payload); +#else + return pure_app_message(app, input, payload); +#endif +} #include <stdbool.h> #include <stdint.h> #include <stdio.h> @@ -38,13 +46,13 @@ static bool load_graph(PureApp *app, const PureModule *m, uint32_t nodes, size_t size_t unique = graph_build(nodes, edges, &keys); unsigned char four[4]; memcpy(four, &nodes, 4); - if (pure_app_message(app, (uint32_t)pure_module_input(m, "size"), NULL, -1, (PureSpan){four, 4}) != PURE_OK) return false; + if (send_message(app, (uint32_t)pure_module_input(m, "size"), (PureSpan){four, 4}) != PURE_OK) return false; enum { CHUNK = 65536 }; double *rows = malloc(CHUNK * 2 * sizeof(*rows)); for (size_t at = 0; at < unique; at += CHUNK) { size_t n = unique - at < CHUNK ? unique - at : CHUNK; for (size_t k = 0; k < n; ++k) rows[2 * k] = (double)keys[at + k], rows[2 * k + 1] = (double)(keys[at + k] & 0xFFFFF); - if (pure_app_message(app, (uint32_t)pure_module_input(m, "load"), NULL, -1, (PureSpan){(unsigned char *)rows, n * 16}) != + if (send_message(app, (uint32_t)pure_module_input(m, "load"), (PureSpan){(unsigned char *)rows, n * 16}) != PURE_OK) return false; } @@ -94,7 +102,7 @@ static int run(const char *dir, const Work *w, unsigned rounds) { unsigned char bytes[8]; memcpy(bytes, args, 4 * w->argc); double a = now_ms(); - PureResult result = pure_app_message(&app, (uint32_t)input, NULL, -1, (PureSpan){bytes, 4 * w->argc}); + PureResult result = send_message(&app, (uint32_t)input, (PureSpan){bytes, 4 * w->argc}); double b = now_ms(); if (result != PURE_OK) { fprintf(stderr, "%s failed: %s\n", w->name, pure_fault_name(app.fault)); return 1; } double out[4] = {0}; diff --git a/research/experiments/041-batches/2026-10-18-counts.jsonl b/research/experiments/041-batches/2026-10-18-counts.jsonl index 234a049..3fdfe04 100644 --- a/research/experiments/041-batches/2026-10-18-counts.jsonl +++ b/research/experiments/041-batches/2026-10-18-counts.jsonl @@ -78,3 +78,13 @@ {"version":"b32","way":"rounds","work":"matmul","instructions":21257327279,"cycles":7241395477} {"version":"b32","way":"batches","work":"roots","instructions":157304888,"cycles":109458185} {"version":"b32","way":"rounds","work":"roots","instructions":2719817748,"cycles":1072835977} +{"version":"b33","way":"batches","work":"matmul","instructions":713397270,"cycles":229071294} +{"version":"b33","way":"rounds","work":"matmul","instructions":21259727715,"cycles":6948994548} +{"version":"b33","way":"batches","work":"knapsack","instructions":348627409,"cycles":105944635} +{"version":"b33","way":"rounds","work":"knapsack","instructions":21991044035,"cycles":7804958171} +{"version":"b33","way":"batches","work":"roots","instructions":157304942,"cycles":109407347} +{"version":"b33","way":"rounds","work":"roots","instructions":2719818048,"cycles":1007507681} +{"version":"b33","way":"batches","work":"join","instructions":1783713585,"cycles":1030218994} +{"version":"b33","way":"rounds","work":"join","instructions":7680473996,"cycles":3365659328} +{"version":"b33","way":"batches","work":"bfs","instructions":3345467558,"cycles":1676207366} +{"version":"b33","way":"rounds","work":"bfs","instructions":4703743508,"cycles":2071781377} diff --git a/serve/deliver.c b/serve/deliver.c index d56932d..f239ccd 100644 --- a/serve/deliver.c +++ b/serve/deliver.c @@ -267,8 +267,8 @@ uint32_t pure_wire_output(const PureWire *w) { return w->output; } bool pure_wire_reply(PureWire *w, uint32_t input, char *error, size_t size) { const PureModule *m = w->module; - if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || m->input[input].keyed) { - snprintf(error, size, "an answer goes to a message input without a key"); + if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE) { + snprintf(error, size, "an answer goes to a message input"); return false; } w->answer_input = (int)input; diff --git a/serve/serve.c b/serve/serve.c index bb339cf..500f470 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -9,10 +9,7 @@ * its bytes field from the body, and answers what its handler replies, a * field of the message). A failed event answers with its fault. * - * A module with keyed messages named show and edit also serves the document - * contract of server.c (GET and PUT /objects/KEY, If-Match or If-None-Match: - * *, ETag = revision, X-Pure-Error): GET is its show message and PUT its edit - * message. HTTP/1.1 connections persist and may pipeline; an epoll thread + * HTTP/1.1 connections persist and may pipeline; an epoll thread * accepts, reads and writes without blocking. Requests go through the * runtime core (a work slot per connection, the payload budget, ingress * owned by the core, write replies borrowed from the request). After each @@ -23,9 +20,8 @@ * * With --threads N each thread has its own listener on the same port * (SO_REUSEPORT), epoll set and runtime domain; the kernel spreads - * connections over them. A module of keyed documents alone runs one app per - * thread: they share nothing but the store, whose commits take turns and - * whose revisions settle concurrent edits of a key. States have one owner: + * connections over them. A module without states or outputs runs one app + * per thread: there is nothing for them to share. States have one owner: * for a module with them, one more thread owns the app, and the connection * threads hand it their ready requests and answer what it hands back. Its * bursts take the requests of every thread, one save each. */ @@ -74,7 +70,7 @@ typedef struct Host Host; /* What a request names: a document, a view (a table's rows from, count) or * an input. Kept per work slot, so an accepted event runs even if its * connection is gone before the burst. */ -typedef enum { ROUTE_NONE, ROUTE_OBJECT, ROUTE_VIEW, ROUTE_INPUT, ROUTE_QUERY } RouteKind; +typedef enum { ROUTE_NONE, ROUTE_VIEW, ROUTE_INPUT, ROUTE_QUERY } RouteKind; /* A view's (or a query's) arguments come as the request's query: argued * says so, and given is its text, kept on the request's work slot. */ typedef struct App App; @@ -172,7 +168,6 @@ typedef struct Connection { Seal seal; char aad[PURE_SOURCE + 48]; size_t opened; - int64_t expected; size_t body_size, received, in_used; unsigned char *body; PureTicket ticket; @@ -195,7 +190,6 @@ typedef struct { PureTicket ticket; PureInput input; PureResult status; - int64_t revision; PureResource resource; PureSpan reply; struct iovec *extents; @@ -221,8 +215,8 @@ typedef struct Published { _Atomic unsigned subscribers; /* streams of its views on any thread (docs/design/019): publishing wakes their threads */ } Published; -/* The app and its store. A connection thread runs it itself (an app of - * documents only), or one owner thread runs it for all connection threads. +/* The app and its store. A connection thread runs it itself (an app without + * states or outputs), or one owner thread runs it for all connection threads. * The owner publishes a snapshot of the states after each save, and the * connection threads show views from it themselves: reads never wait for * the owner, which may be applying events or waiting for a sync. */ @@ -230,7 +224,6 @@ typedef struct { const PureModule *module; PureStore *store; PureApp app; - uint32_t show, edit; uint64_t bursts, burst_events; Applied *applied; /* a round's requests, grown to fit */ size_t applied_capacity; @@ -425,9 +418,7 @@ struct App { char *tokens_path; uint64_t tokens_digest; const char *store; - bool documents; /* the module serves /objects: it has show and edit */ bool owned; /* an owner runs it (states or outputs, or any of --apps); else the connection threads */ - uint32_t show, edit; _Atomic size_t memory; /* its budgets (0: none): what its states hold (read by its views' readers too), */ _Atomic uint64_t work; /* what an event or a view's evaluation may run */ size_t queue; /* the requests that may wait for its turn, and its views' with the view workers */ @@ -459,7 +450,7 @@ static void subscribe(Connection *c, const Route *route, const char *raw, size_t static void wake_pushers(void); struct Host { - Executor local; /* the one app of the host, when this thread runs it (documents alone) */ + Executor local; /* the one app of the host, when this thread runs it (no states nor outputs) */ PureApp *readers; /* by app: shows its owner's snapshots here */ PureSnapshot **snapshots; /* by app: the snapshot a round reads views from, once */ uint64_t round, *read_in, *handed_in; /* by app: the round it was read in, its requests handed in */ @@ -597,8 +588,8 @@ static void put_decimal(Head *h, uint64_t value) { /* Answer with a body of `count` extents (`size` bytes in all); the ticket * keeps them alive. One writev sends as much as the socket takes. */ -static void respond_typed(Connection *c, int status, const char *reason, const char *error, int64_t revision, - const char *type, const struct iovec *extents, size_t count, size_t size) { +static void respond_typed(Connection *c, int status, const char *reason, const char *error, const char *type, + const struct iovec *extents, size_t count, size_t size) { Head h = {c->head, c->head + sizeof(c->head)}; put_text(&h, "HTTP/1.1 "); put_decimal(&h, (uint64_t)status); @@ -612,11 +603,6 @@ static void respond_typed(Connection *c, int status, const char *reason, const c put_text(&h, "\r\nCache-Control: no-store\r\nX-Pure-Error: "); put_text(&h, error); put_text(&h, status == 401 ? "\r\nWWW-Authenticate: Bearer\r\n" : "\r\n"); - if (revision > 0) { - put_text(&h, "ETag: \""); - put_decimal(&h, (uint64_t)revision); - put_text(&h, "\"\r\n"); - } /* A delivery's answer tells how many numbered messages one request may carry. */ if (*c->source) { put_text(&h, "X-Pure-Batch: "); @@ -634,43 +620,37 @@ static void respond_typed(Connection *c, int status, const char *reason, const c c->out_at = 0; } -static void respond_extents(Connection *c, int status, const char *reason, const char *error, int64_t revision, - const struct iovec *extents, size_t count, size_t size) { - respond_typed(c, status, reason, error, revision, "application/octet-stream", extents, count, size); -} - -static void respond(Connection *c, int status, const char *reason, const char *error, int64_t revision, - PureSpan body) { +static void respond(Connection *c, int status, const char *reason, const char *error, PureSpan body) { struct iovec extent = {(void *)body.data, body.size}; - respond_extents(c, status, reason, error, revision, &extent, body.size ? 1 : 0, body.size); + respond_typed(c, status, reason, error, "application/octet-stream", &extent, body.size ? 1 : 0, body.size); } /* A request refused before it has a ticket; a body it came with is not read. */ static void reject(Connection *c, int status, const char *reason, const char *error) { if (c->body_size) c->keep_alive = false; - respond(c, status, reason, error, 0, (PureSpan){0}); + respond(c, status, reason, error, (PureSpan){0}); } /* A request the host cannot read further: answer and close. */ static void refuse(Connection *c, int status, const char *reason, const char *error) { release_ticket(c); c->keep_alive = false; - respond(c, status, reason, error, 0, (PureSpan){0}); + respond(c, status, reason, error, (PureSpan){0}); } static void answer(Connection *c, PureOutcome result) { switch (result.status) { - case PURE_OK: respond(c, 200, "OK", "none", result.revision, result.body); return; - case PURE_CONFLICT: respond(c, 412, "Precondition Failed", "conflict", result.revision, (PureSpan){0}); return; - case PURE_NOT_FOUND: respond(c, 404, "Not Found", "not_found", 0, (PureSpan){0}); break; - case PURE_BUSY: respond(c, 503, "Service Unavailable", "store_busy", 0, (PureSpan){0}); break; - case PURE_BUDGET: respond(c, 503, "Service Unavailable", "payload_budget", 0, (PureSpan){0}); break; - case PURE_ALLOCATION: respond(c, 503, "Service Unavailable", "allocation", 0, (PureSpan){0}); break; - case PURE_CLOSED: respond(c, 503, "Service Unavailable", "stopped", 0, (PureSpan){0}); break; - case PURE_DENIED: respond(c, 403, "Forbidden", "denied", 0, (PureSpan){0}); break; - case PURE_LIMIT: respond(c, 413, "Content Too Large", "limit", 0, (PureSpan){0}); break; - case PURE_UNCERTAIN: respond(c, 500, "Internal Server Error", "durability_unknown", 0, (PureSpan){0}); break; - default: respond(c, 500, "Internal Server Error", "storage", 0, (PureSpan){0}); break; + case PURE_OK: respond(c, 200, "OK", "none", result.body); return; + case PURE_CONFLICT: respond(c, 409, "Conflict", "conflict", (PureSpan){0}); break; + case PURE_NOT_FOUND: respond(c, 404, "Not Found", "not_found", (PureSpan){0}); break; + case PURE_BUSY: respond(c, 503, "Service Unavailable", "store_busy", (PureSpan){0}); break; + case PURE_BUDGET: respond(c, 503, "Service Unavailable", "payload_budget", (PureSpan){0}); break; + case PURE_ALLOCATION: respond(c, 503, "Service Unavailable", "allocation", (PureSpan){0}); break; + case PURE_CLOSED: respond(c, 503, "Service Unavailable", "stopped", (PureSpan){0}); break; + case PURE_DENIED: respond(c, 403, "Forbidden", "denied", (PureSpan){0}); break; + case PURE_LIMIT: respond(c, 413, "Content Too Large", "limit", (PureSpan){0}); break; + case PURE_UNCERTAIN: respond(c, 500, "Internal Server Error", "durability_unknown", (PureSpan){0}); break; + default: respond(c, 500, "Internal Server Error", "storage", (PureSpan){0}); break; } /* A failed request carries no body: the payload can go now. */ if (!result.body.size) release_ticket(c); @@ -684,18 +664,18 @@ static void answer_app(Connection *c, PureOutcome result, bool view, PureFault f const char *named = fault != PURE_FAULT_NONE ? pure_fault_name(fault) : NULL; if (result.status == PURE_OK) { struct iovec extent = {(void *)result.body.data, result.body.size}; - respond_typed(c, 200, "OK", "none", 0, type ? type : "application/octet-stream", &extent, + respond_typed(c, 200, "OK", "none", type ? type : "application/octet-stream", &extent, result.body.size ? 1 : 0, result.body.size); } else if (result.status == PURE_NOT_FOUND) { - respond(c, 404, "Not Found", view ? "no_value" : named ? named : "not_found", 0, (PureSpan){0}); + respond(c, 404, "Not Found", view ? "no_value" : named ? named : "not_found", (PureSpan){0}); } else if (result.status == PURE_CONFLICT) { - respond(c, 409, "Conflict", named ? named : "conflict", 0, (PureSpan){0}); + respond(c, 409, "Conflict", named ? named : "conflict", (PureSpan){0}); } else if (fault == PURE_FAULT_MEMORY) { - respond(c, 503, "Service Unavailable", named, 0, (PureSpan){0}); + respond(c, 503, "Service Unavailable", named, (PureSpan){0}); } else if (named && (result.status == PURE_LIMIT || result.status == PURE_INVALID)) { - respond(c, 422, "Unprocessable Content", named, 0, (PureSpan){0}); + respond(c, 422, "Unprocessable Content", named, (PureSpan){0}); } else if (result.status == PURE_INVALID) { - respond(c, 400, "Bad Request", "invalid", 0, (PureSpan){0}); /* such as a text that is not UTF-8 */ + respond(c, 400, "Bad Request", "invalid", (PureSpan){0}); /* such as a text that is not UTF-8 */ } else { answer(c, result); return; @@ -765,7 +745,7 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { if (end) path += 1 + length; } static const struct { const char *prefix; RouteKind kind; } routes[] = { - {"/objects/", ROUTE_OBJECT}, {"/views/", ROUTE_VIEW}, {"/inputs/", ROUTE_INPUT}, {"/queries/", ROUTE_QUERY}}; + {"/views/", ROUTE_VIEW}, {"/inputs/", ROUTE_INPUT}, {"/queries/", ROUTE_QUERY}}; const char *key = ""; c->route = ROUTE_NONE; for (size_t i = 0; i < sizeof(routes) / sizeof(*routes) && c->route == ROUTE_NONE; ++i) @@ -781,12 +761,11 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { return false; } memcpy(c->key, key, key_size + 1); - /* Each route has its method: documents GET and PUT, views GET, inputs - * POST, queries PUT (registered) and GET (shown). */ - if ((c->route == ROUTE_OBJECT && (c->post || *query)) || (c->route == ROUTE_VIEW && (c->put || c->post)) || + /* Each route has its method: views GET, inputs POST, queries PUT (registered) and GET (shown). */ + if ((c->route == ROUTE_VIEW && (c->put || c->post)) || (c->route == ROUTE_INPUT && !c->post) || (c->route == ROUTE_QUERY && (c->post || (c->put && *query)))) return false; - bool length_seen = false, condition_seen = false, host_seen = false; + bool length_seen = false, host_seen = false; c->source[0] = 0; c->sequence = 0; c->batch = 1; @@ -796,7 +775,6 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { bool counted = false; c->body_size = 0; c->json = false; - c->expected = -1; c->keep_alive = true; line = next + 2; while (*line) { @@ -820,16 +798,6 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { } else if (!strcasecmp(line, "Host")) { if (host_seen || !*value) return false; host_seen = true; - } else if (!strcasecmp(line, "If-Match")) { - if (condition_seen || tail - value < 3 || *value != '"' || tail[-1] != '"') return false; - tail[-1] = 0; - if (!number(value + 1, INT64_MAX, &n) || !n) return false; - c->expected = (int64_t)n; - condition_seen = true; - } else if (!strcasecmp(line, "If-None-Match")) { - if (condition_seen || strcmp(value, "*")) return false; - c->expected = 0; - condition_seen = true; } else if (!strcasecmp(line, "Content-Type")) { c->json = !strncasecmp(value, "application/json", 16); } else if (!strcasecmp(line, "Accept")) { @@ -865,10 +833,10 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { } if (!*c->source != !c->sequence || (*c->source && c->route != ROUTE_INPUT) || (counted && !*c->source) || c->sequence > UINT64_MAX - c->batch) return false; - /* A document's PUT names the revision it was made against; a query's replaces what was registered. */ - if (c->put) return host_seen && length_seen && condition_seen == (c->route != ROUTE_QUERY); - if (c->post) return host_seen && !condition_seen; /* no Content-Length: no body */ - return host_seen && !condition_seen && c->body_size == 0; + /* A query's PUT replaces what was registered. */ + if (c->put) return host_seen && length_seen; + if (c->post) return host_seen; /* no Content-Length: no body */ + return host_seen && c->body_size == 0; } static void put_u32(unsigned char *p, uint32_t v) { for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(v >> (8 * i)); } @@ -911,9 +879,7 @@ static const char *resolve(Connection *c, char *query, Route *route, int *status } route->app = app; route->module = c->module = m; - if (route->kind == ROUTE_NONE || (route->kind == ROUTE_OBJECT && !m->collections) || - (route->kind == ROUTE_QUERY && !app->owned)) return "route"; /* a query reads an app's states */ - if (route->kind == ROUTE_OBJECT) return NULL; + if (route->kind == ROUTE_NONE || (route->kind == ROUTE_QUERY && !app->owned)) return "route"; /* a query reads states */ if (route->kind == ROUTE_QUERY) { /* the view is the query module's, found when shown */ route->from = 0; route->count = SIZE_MAX; @@ -945,7 +911,6 @@ static const char *resolve(Connection *c, char *query, Route *route, int *status route->count = SIZE_MAX; const PureInputShape *shape = route->kind == ROUTE_INPUT ? &m->input[found] : NULL; bool message = shape && shape->kind == PURE_KIND_MESSAGE; - if (message && shape->keyed) return "keyed"; if (*c->source && !message) return "delivery"; if (!app->owned) c->source[0] = 0; /* an app without states or outputs applies it as a message */ if (shape) { @@ -1115,14 +1080,14 @@ static bool open_seal(Connection *c) { static void body_done(Connection *c) { if (*c->seal.suite && !open_seal(c)) { release_ticket(c); /* never made ready: it never runs */ - respond(c, 400, "Bad Request", "seal", 0, (PureSpan){0}); + respond(c, 400, "Bad Request", "seal", (PureSpan){0}); return; } const char *why = c->route == ROUTE_INPUT && c->json ? convert(c) : NULL; if (why) { release_ticket(c); /* never made ready: it never runs */ - if (!strcmp(why, "payload_budget") || !strcmp(why, "memory")) respond(c, 503, "Service Unavailable", why, 0, (PureSpan){0}); - else respond(c, 400, "Bad Request", why, 0, (PureSpan){0}); + if (!strcmp(why, "payload_budget") || !strcmp(why, "memory")) respond(c, 503, "Service Unavailable", why, (PureSpan){0}); + else respond(c, 400, "Bad Request", why, (PureSpan){0}); return; } if (!pure_runtime_ready(c->ticket)) abort(); @@ -1205,7 +1170,7 @@ static void start_request(Connection *c, size_t head_size) { } /* A view's stream (docs/design/019): pushed from snapshots, so an owned app's. */ if (c->events && route.kind == ROUTE_VIEW && route.app->owned) { subscribe(c, &route, raw, raw_size); return; } - /* A document's PUT carries its body; an input's event its payload's size, + /* A query's PUT carries its body; an input's event its payload's size, * the payload (the fixed fields, then the body), and room for its JSON. */ bool write = route.kind == ROUTE_INPUT || c->put; size_t table = route.kind == ROUTE_INPUT && c->json ? 8 * (size_t)route.module->input[c->input].field_count : 0; @@ -1213,8 +1178,7 @@ static void start_request(Connection *c, size_t head_size) { /* A batch has a table and a prefix for each of its messages. */ size_t input_size = route.kind == ROUTE_INPUT ? lead + c->batch * (table + c->field_bytes) + c->body_size : write ? c->body_size : 0; - PureResult admitted = pure_runtime_accept(&host->runtime, write ? PURE_INPUT_WRITE : PURE_INPUT_READ, c->key, - route.kind == ROUTE_INPUT || route.kind == ROUTE_QUERY ? 0 : c->expected, + PureResult admitted = pure_runtime_accept(&host->runtime, write ? PURE_INPUT_WRITE : PURE_INPUT_READ, c->key, 0, input_size, &c->ticket); if (admitted != PURE_OK) { c->ticket = (PureTicket){0}; @@ -1394,13 +1358,6 @@ static bool lend(Applied *out, PureMapping *mapping, Gather *gather, bool read, return true; } -static bool copy_reply(void *context, PureSpan span) { - Reply *r = context; - if (span.size) memcpy(r->bytes + r->size, span.data, span.size); - r->size += span.size; - return true; -} - /* ---- App routes -------------------------------------------------------- */ typedef struct { unsigned char *bytes; size_t size, capacity; bool failed; } Output; @@ -1701,8 +1658,8 @@ static PureResult apply_input(PureApp *app, const Route *route, const PureEffect } if (batch > 1 && app_refused(app)) status = PURE_OK; } else - status = table ? pure_app_message_fields(app, route->target, NULL, -1, payload, placed) : - pure_app_message(app, route->target, NULL, -1, payload); + status = table ? pure_app_message_fields(app, route->target, payload, placed) : + pure_app_message(app, route->target, payload); Reply reply = {0}; if (status == PURE_OK) pure_app_reply(app, measure_reply, &reply); out->reply = (PureSpan){reply.size ? reply.first : NULL, reply.size}; @@ -1721,40 +1678,10 @@ static void apply_effect(Executor *ex, const Job *job, Applied *out) { out->status = show_view(app, &job->route, effect->available, out); return; } - if (job->route.kind == ROUTE_INPUT) { - out->status = apply_input(app, &job->route, effect, out); - if (out->status == PURE_OK) ++ex->burst_events; - ex->memory_refused += out->fault == PURE_FAULT_MEMORY; - return; - } - bool read = effect->input == PURE_INPUT_READ; - out->status = pure_app_message(app, read ? ex->show : ex->edit, effect->key, - read ? -1 : effect->expected, read ? (PureSpan){0} : effect->value); - out->revision = app->revision; - if (out->status != PURE_OK) return; - ++ex->burst_events; - Reply reply = {0}; - pure_app_reply(app, measure_reply, &reply); - if (!read) { - /* Checked at start: an edit replies a field of its request. */ - out->reply = (PureSpan){reply.size ? reply.first : NULL, reply.size}; - return; - } - if (reply.size > effect->available) { out->status = PURE_BUDGET; return; } - if (!reply.size) return; - /* A stored document is lent, extent by extent, not copied. */ - PureMapping *mapping = app->document ? pure_draft_mapping(app->document) : NULL; - if (mapping) { - Gather gather = {NULL, 0, 0, mapping, true}; - bool read = pure_app_reply(app, gather_extents, &gather) == PURE_OK; - if (lend(out, mapping, &gather, read, reply.size)) return; - } - unsigned char *copy = malloc(reply.size); - if (!copy) { out->status = PURE_ALLOCATION; return; } - reply = (Reply){.bytes = copy}; - pure_app_reply(app, copy_reply, &reply); - out->resource = (PureResource){.body = {copy, reply.size}, .allocation = copy, .allocation_size = reply.size, - .release = release_copy}; + /* An input's event (what reaches an app's owner is a view or an input) */ + out->status = apply_input(app, &job->route, effect, out); + if (out->status == PURE_OK) ++ex->burst_events; + ex->memory_refused += out->fault == PURE_FAULT_MEMORY; } /* ---- A surface in the node (serve/surface.h): one, run in its own thread, @@ -1869,7 +1796,7 @@ static bool local_queue(Local *l, uint64_t seq, PureSpan payload) { * why in error, when r has none. */ static bool local_bind(Local *l, const PureModule *r, char *error, size_t size) { int input = pure_module_input(r, l->input_name); - if (input < 0 || r->input[input].kind != PURE_KIND_MESSAGE || r->input[input].keyed) { + if (input < 0 || r->input[input].kind != PURE_KIND_MESSAGE) { snprintf(error, size, "no input '%s' taking messages", l->input_name); return false; } @@ -2141,10 +2068,8 @@ static void save_burst(Executor *ex, Applied *applied, size_t count) { if (applied[i].status == PURE_OK) applied[i].status = saved; } -/* Run a round's requests in bursts. A burst ends before a document's key - * repeats or before its documents would outgrow one save, so every request - * is stored by its burst's save or fails with it; views and events of the - * app's states join any burst. Each group of answers goes to finish. +/* Run a round's requests in a burst: every request is stored by its save or + * fails with it. Each group of answers goes to finish. * * The owner takes a round's reads first and answers them at once: a * round's requests come from different connections (each has one request @@ -2167,20 +2092,9 @@ static void execute(Executor *ex, const Job *jobs, size_t count, void (*finish)( for (size_t i = 0; ex->reads_first && i < count; ++i) if (jobs[i].effect.input == PURE_INPUT_READ) apply_effect(ex, &jobs[i], &applied[reads++]); if (reads) finish(context, applied, reads); - size_t n = reads, start = reads, keys = 0; - for (size_t i = 0; i < count; ++i) { - if (ex->reads_first && jobs[i].effect.input == PURE_INPUT_READ) continue; - /* A document joins a burst once (its revision moves once a save). */ - bool document = jobs[i].route.kind == ROUTE_OBJECT; - if (document && (keys == PURE_APP_PENDING || pure_app_pending(&ex->app, jobs[i].effect.key))) { - save_burst(ex, applied + start, n - start); - finish(context, applied + start, n - start); - start = n; - keys = 0; - } - apply_effect(ex, &jobs[i], &applied[n++]); - keys += document; - } + size_t n = reads, start = reads; + for (size_t i = 0; i < count; ++i) + if (!ex->reads_first || jobs[i].effect.input != PURE_INPUT_READ) apply_effect(ex, &jobs[i], &applied[n++]); save_burst(ex, applied + start, n - start); finish(context, applied + start, n - start); if (pinned) pure_store_pin(ex->store, false); @@ -2218,20 +2132,19 @@ static long run_timers(Executor *ex) { * its connection still waits for it. */ static void answer_applied(Host *host, Applied *a) { if (!pure_runtime_complete(a->ticket, a->status, a->input == PURE_INPUT_READ ? &a->resource : NULL, - a->status == PURE_OK ? a->reply : (PureSpan){0}, a->revision)) abort(); + a->status == PURE_OK ? a->reply : (PureSpan){0}, 0)) abort(); Connection *c = host->by_slot[a->ticket.slot]; if (c && c->phase == WAITING && c->ticket.generation == a->ticket.generation) { PureOutcome result; if (!pure_runtime_result(a->ticket, &result)) abort(); /* A lent answer's body is its extents (the runtime charged them). */ if (result.status == PURE_OK && a->extents) - respond_typed(c, 200, "OK", "none", result.revision, a->type ? a->type : "application/octet-stream", + respond_typed(c, 200, "OK", "none", a->type ? a->type : "application/octet-stream", a->extents, a->extent_count, result.body.size); else if (a->refused) { - respond(c, 503, "Service Unavailable", a->refused, 0, (PureSpan){0}); + respond(c, 503, "Service Unavailable", a->refused, (PureSpan){0}); release_ticket(c); - } else if (a->route != ROUTE_OBJECT) answer_app(c, result, a->route == ROUTE_VIEW, a->fault, a->type); - else answer(c, result); + } else answer_app(c, result, a->route == ROUTE_VIEW, a->fault, a->type); drive(c); /* most answers fit the socket buffer at once */ } free(a->extents); @@ -2812,11 +2725,9 @@ static void let_go_query(Held *held) { } /* Whether a's states are laid out as b's: names, kinds, what each refers to, - * keys, and the records and document collections behind them. */ + * keys, and the records behind them. */ static bool same_states(const PureModule *a, const PureModule *b) { - if (a->states != b->states || a->records != b->records || a->record_fields != b->record_fields || - a->collections != b->collections) - return false; + if (a->states != b->states || a->records != b->records || a->record_fields != b->record_fields) return false; for (uint32_t i = 0; i < a->states; ++i) if (strcmp(a->state_names[i], b->state_names[i]) || a->state_type[i] != b->state_type[i] || a->state_ref[i] != b->state_ref[i] || a->state_flags[i] != b->state_flags[i] || a->state_key[i] != b->state_key[i]) @@ -2828,8 +2739,6 @@ static bool same_states(const PureModule *a, const PureModule *b) { for (uint32_t i = 0; i < a->record_fields; ++i) if (strcmp(a->record_field_names[i], b->record_field_names[i]) || a->record_field_type[i] != b->record_field_type[i]) return false; - for (uint32_t i = 0; i < a->collections; ++i) - if (strcmp(a->collection_names[i], b->collection_names[i])) return false; return true; } @@ -2919,7 +2828,7 @@ static void serve_bursts(Host *host) { while (pure_runtime_take(&host->runtime, &effect) == PURE_TAKE_EFFECT) host->jobs[count++] = (Job){host, effect, host->routes[effect.ticket.slot], NULL, NULL}; if (!count) return; - if (!apps[0].owned) { /* the one app of the host, of documents alone: run here */ + if (!apps[0].owned) { /* the one app of the host, without states or outputs: run here */ execute(&host->local, host->jobs, count, answer_burst, NULL); continue; } @@ -3321,12 +3230,10 @@ static bool replies_borrow(const PureModule *m, uint32_t input) { /* ---- Main ------------------------------------------------------------- */ -/* A module from its file for an app, which serves documents when it keeps - * a keyed collection; false, with why, when it does not load or cannot be - * served. The module the app had is kept (see retired_modules). */ +/* A module from its file for an app; false, with why, when it does not load + * or cannot be served. The module the app had is kept (see retired_modules). */ static bool load_module(App *app, const char *path) { PureModule *m = calloc(1, sizeof(*m)); - bool documents; char error[160]; if (!m) { snprintf(app->why, sizeof(app->why), "Module: out of memory"); @@ -3337,17 +3244,9 @@ static bool load_module(App *app, const char *path) { free(m); return false; } - /* Documents are served when the module keeps a keyed collection: its keyed - * messages show and edit read and write one by key. Without one, inputs - * named show and edit are messages as any other. */ - int show = pure_module_input(m, "show"), edit = pure_module_input(m, "edit"); - documents = m->collections > 0; - bool served = !documents || (show >= 0 && edit >= 0 && m->input[show].kind == PURE_KIND_MESSAGE && m->input[show].keyed && - m->input[edit].kind == PURE_KIND_MESSAGE && m->input[edit].keyed); - if (!served) snprintf(app->why, sizeof(app->why), "Module: documents need keyed messages named show and edit"); + bool served = true; for (uint32_t i = 0; served && i < m->inputs; ++i) { - bool replied = (documents && i == (uint32_t)edit) || (m->input[i].kind == PURE_KIND_MESSAGE && !m->input[i].keyed); - if (replied && !replies_borrow(m, i)) { + if (m->input[i].kind == PURE_KIND_MESSAGE && !replies_borrow(m, i)) { snprintf(app->why, sizeof(app->why), "Module: %s may reply only a field of its message", m->input_names[i]); served = false; } @@ -3367,9 +3266,6 @@ static bool load_module(App *app, const char *path) { retired_modules = kept; } app->module = m; - app->documents = documents; - app->show = (uint32_t)(show >= 0 ? show : 0); - app->edit = (uint32_t)(edit >= 0 ? edit : 0); return true; } @@ -3490,8 +3386,6 @@ static void take_moved(Host *host) { * when the store does not open or the module does not start on it. */ static bool start_executor(Executor *ex, App *app) { ex->module = app->module; - ex->show = app->show; - ex->edit = app->edit; if (pure_store_open(app->store, &ex->store) != PURE_OK) { snprintf(app->why, sizeof(app->why), "Cannot open the store"); return false; @@ -3702,8 +3596,7 @@ static void stop_shard(Host *host) { free(host->moved); } -/* The input a link's stream goes to: one taking messages (not keyed) of the - * sender's shape, or, named its fields, one that takes them by name +/* The input a link's stream goes to: one taking messages of the sender's shape, or, named its fields, one that takes them by name * (pure_module_takes: each record is laid out anew for it). */ /* In a host of apps the name is APP/INPUT. What it finds is a binding: the * app's place, the module it runs and the input in it, the listener @@ -3725,7 +3618,7 @@ static int link_input(void *context, const char *name, uint32_t shape, const uns } const PureModule *m = atomic_load_explicit(&apps[a].live, memory_order_acquire); int index = m ? pure_module_input(m, name) : -1; - if (index < 0 || m->input[index].kind != PURE_KIND_MESSAGE || m->input[index].keyed) return -PURE_LINK_NO_INPUT; + if (index < 0 || m->input[index].kind != PURE_KIND_MESSAGE) return -PURE_LINK_NO_INPUT; if (pure_module_shape(m, (uint32_t)index) != shape && !fields) return -PURE_LINK_OTHER_SHAPE; if (pure_module_shape(m, (uint32_t)index) != shape && !pure_module_takes(m, (uint32_t)index, fields, size)) return -PURE_LINK_FIELDS_LACKED; @@ -4527,7 +4420,6 @@ static bool change_app(App *app) { return false; } ex->module = app->module; - ex->show = app->show, ex->edit = app->edit; memset(ex->next, 0, sizeof(ex->next)); memset(ex->trimmed, 0, sizeof(ex->trimmed)); memset(ex->held, 0, sizeof(ex->held)); diff --git a/tests/check.py b/tests/check.py index 8814add..1e14e12 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3,14 +3,14 @@ aup builds (the sibling checkout, --aup-root). The core's and the store's own tests are core/test-module.c and store/test-tree-store.c. -1. Documents: pure-serve running the document editor written in aup - (aup's examples/editor.aup) answers random requests exactly as the earlier - document server running editor.pure, with one and with three connection +1. Documents: keyed byte documents as a keyed list (DOCS_SOURCE, each edit + checked against the revision it was made on) answer random edits and + reads exactly as a model says, with one and with three connection threads, on persistent and pipelined connections too. -2. Lending: a read lends the stored extents and still carries its version - while the document changes; lent answers count against the payload budget; - a text view is lent while its answer waits and let go when it is read or - its connection closes, the store keeping one mapping again. +2. Lending: a text view lends the state's stored extents while its answer + waits, each answer whole in one version while the text changes, and lets + them go when it is read or its connection closes, the store keeping one + mapping again; lent answers count against the payload budget. 3. Durability: concurrent edits share commits, and every acknowledged edit survives kill -9, with one and with three threads; so do the app's acknowledged posts (keyed lists, JSON bodies with groups). @@ -39,8 +39,8 @@ from pathlib import Path HERE = Path(__file__).resolve().parent ROOT = HERE.parent -# The app that exercises documents: the editor written in aup, from aup's examples. -EDITOR = ROOT.parent / 'aup' / 'examples' / 'editor.aup' +# aup's examples (its checkout beside this one, or --aup-root). +EXAMPLES = ROOT.parent / 'aup' / 'examples' def run(*args, stdin=None, check=True): @@ -67,19 +67,6 @@ class HttpHost: self.info = json.loads(line) # its port, name, public key self.port = self.info['port'] - def request(self, method, key, body=b'', expected=None): - import http.client - headers = {'Content-Length': str(len(body))} - if expected is not None: - headers['If-None-Match' if expected == 0 else 'If-Match'] = '*' if expected == 0 else f'"{expected}"' - connection = http.client.HTTPConnection('127.0.0.1', self.port, timeout=30) - try: - connection.request(method, f'/objects/{key}', body, headers) - response = connection.getresponse() - return response.status, response.getheader('ETag'), response.getheader('X-Pure-Error'), response.read() - finally: - connection.close() - def stop(self): self.process.terminate() _, error = self.process.communicate(timeout=30) @@ -87,48 +74,146 @@ class HttpHost: return [json.loads(line) for line in error.decode().splitlines() if line.startswith('{')] -def server_parity(aup, pure, work, seeds, operations=150, threads=1): - """The document server running editor.aup answers every request exactly - as the server running editor.pure: status, revision, error and body.""" - import struct - module = work / 'editor-server.pmod' - compile_module(aup, EDITOR, module) +DOCS_SOURCE = """record Doc { + id: text + body: bytes + revision: i64 +} +state docs: list of Doc by id + +input edit = message { + id: text + base: i64 + start: u32 + remove: u32 + insert: bytes +} + +on edit { + if docs.has(edit.id) { + let d = docs[edit.id] + require d.revision == edit.base + require edit.start + edit.remove <= d.body.bytes + docs[edit.id].body = d.body[0 .. edit.start] + edit.insert + d.body[edit.start + edit.remove .. d.body.bytes] + docs[edit.id].revision = d.revision + 1 + } else { + require edit.base == 0 && edit.start == 0 && edit.remove == 0 + docs.add(Doc { id: edit.id, body: edit.insert, revision: 1 }) + } + reply edit.insert +} + +view revision(id: text) = docs[id].revision +view size(id: text) = docs[id].body.bytes +view body(id: text) = text(docs[id].body) +""" + + +def edit_body(key, base, start, remove, insert): + """An edit of DOCS_SOURCE as its JSON body.""" + return json.dumps({'id': key, 'base': base, 'start': start, 'remove': remove, 'insert': insert}).encode() + + +def edit_request(key, base, start, remove, insert): + body = edit_body(key, base, start, remove, insert) + return (b'POST /inputs/edit HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: ' + + str(len(body)).encode() + b'\r\n\r\n' + body) + + +def view_request(view, key): + from urllib.parse import quote + return f'GET /views/{view}?id={quote(key)} HTTP/1.1\r\nHost: x\r\n\r\n'.encode() + + +def docs_call(port, request): + """One raw request on its own connection: status, X-Pure-Error, body.""" + import socket + with socket.create_connection(('127.0.0.1', port), timeout=30) as sock: + sock.sendall(request) + return read_answer(sock.makefile('rb')) + + +def read_answer(reader): + status = int(reader.readline().split()[1]) + length, error = 0, None + for line in iter(reader.readline, b'\r\n'): + name, _, value = line.decode().partition(':') + if name.lower() == 'content-length': length = int(value) + if name.lower() == 'x-pure-error': error = value.strip() + return status, error, reader.read(length) + + +class DocsModel: + """DOCS_SOURCE as Python: an edit's answer (status, error, body) and each document's revision and body.""" + + def __init__(self): + self.docs = {} + + def edit(self, key, base, start, remove, insert): + body, revision = self.docs.get(key, (b'', 0)) + data = insert.encode() + if key in self.docs: + if base != revision or start + remove > len(body): return 422, 'limit', b'' + self.docs[key] = (body[:start] + data + body[start + remove:], revision + 1) + elif base or start or remove: + return 422, 'limit', b'' + else: + self.docs[key] = (data, 1) + return 200, 'none', data + + def view(self, view, key): + if key not in self.docs: return 404, 'no_value', b'' + body, revision = self.docs[key] + if view == 'body': + try: + body.decode() + except UnicodeDecodeError: # cut inside a character: text(...) refuses it + return 400, 'invalid', b'' + return 200, 'none', body + return 200, 'none', str(revision if view == 'revision' else len(body)).encode() + + +def server_documents(aup, pure, work, seeds, operations=150, threads=1): + """Keyed byte documents as a keyed list (027's 4b; DOCS_SOURCE): random + edits (current, stale and future revisions, past the end, texts of any + size up to 5,000 bytes) and reads of a document's revision, size and + body answer exactly as a model says, with one and with three connection + threads; after a restart every document is as acknowledged.""" + module = work / 'docs.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) compared = 0 + words = ['', 'a', 'hello', '빨리', 'a "quoted" \\ word', '😀', 'line\nbreak'] for seed in seeds: rng = random.Random(seed + 13000) - old = HttpHost([pure / 'prototypes/connections/build/tree/pure-connections', '--program', pure / 'prototypes/connections/examples/editor.pure', - '--store', work / f'http-old-{seed}-{threads}']) - new = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / f'http-new-{seed}-{threads}', - '--threads', str(threads)]) + store = work / f'docs-{seed}-{threads}' + model = DocsModel() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(threads)]) try: - revision, size = {}, {} for _ in range(operations): - key = rng.choice(['a', 'b', 'long-key_1']) - current, length = revision.get(key, 0), size.get(key, 0) + key = rng.choice(['a', 'b', 'long-key_1', '문서']) + body, current = model.docs.get(key, (b'', 0)) if rng.random() < 0.35: - request = ('GET', key, b'', None) + view = rng.choice(['revision', 'size', 'body']) + want, got = model.view(view, key), docs_call(host.port, view_request(view, key)) else: pick = rng.random() base = current if pick < 0.75 else max(current - 1, 0) if pick < 0.9 else current + 3 - if rng.random() < 0.05: - body = bytes(rng.randrange(256) for _ in range(rng.randint(0, 7))) # too short - else: - insert = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 7, 300, 5000]))) - body = struct.pack('<II', rng.randint(0, length + 2), rng.choice([0, 1, 3, length])) + insert - request = ('PUT', key, body, base) - theirs, ours = old.request(*request), new.request(*request) - assert ours == theirs, (seed, request[:2], request[3], ours[:3], theirs[:3]) - if theirs[0] == 200 and theirs[1]: - revision[key] = int(theirs[1].strip('"')) - if request[0] == 'GET': - size[key] = len(theirs[3]) - else: - size[key] = len(old.request('GET', key)[3]) - assert new.request('GET', key) == old.request('GET', key) + insert = ''.join(rng.choice(words) for _ in range(rng.choice([0, 1, 7, 300, 700])))[:5000] + start, remove = rng.randint(0, len(body) + 2), rng.choice([0, 1, 3, len(body)]) + want = model.edit(key, base, start, remove, insert) + got = docs_call(host.port, edit_request(key, base, start, remove, insert)) + assert got == want, (seed, key, got[:2], want[:2]) compared += 1 finally: - old.stop() - new.stop() + host.stop() + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(threads)]) + try: + for key in model.docs: + for view in ('revision', 'size', 'body'): + assert docs_call(host.port, view_request(view, key)) == model.view(view, key), (key, view) + finally: + host.stop() return compared @@ -136,122 +221,77 @@ def server_pipelining(aup, pure, work): """One connection, many requests: a keep-alive client and a pipelined burst (all requests in one write, one split across writes) get the same answers, in order, as separate connections would.""" - import http.client import socket - import struct - module = work / 'editor-pipeline.pmod' - compile_module(aup, EDITOR, module) + module = work / 'docs-pipeline.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'pipeline']) + answers = [] try: - connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=30) - answers = [] - for body, headers in [(struct.pack('<II', 0, 0) + b'hello', {'If-None-Match': '*'}), - (struct.pack('<II', 5, 0) + b'!', {'If-Match': '"1"'}), - (struct.pack('<II', 0, 0) + b'x', {'If-Match': '"1"'})]: - connection.request('PUT', '/objects/p', body, headers) - response = connection.getresponse() - answers.append((response.status, response.getheader('ETag'), response.read())) - connection.request('GET', '/objects/p') - response = connection.getresponse() - answers.append((response.status, response.getheader('ETag'), response.read())) - connection.close() - assert answers == [(200, '"1"', b'hello'), (200, '"2"', b'!'), (412, '"2"', b''), (200, '"2"', b'hello!')], answers - - def request(method, key, body=b'', condition=''): - return (f'{method} /objects/{key} HTTP/1.1\r\nHost: x\r\nContent-Length: {len(body)}\r\n{condition}\r\n' - .encode() + body) - pipeline = (request('GET', 'p') + request('PUT', 'q', struct.pack('<II', 0, 0) + b'q1', 'If-None-Match: *\r\n') + - request('GET', 'q') + request('GET', 'missing') + - request('PUT', 'q', struct.pack('<II', 2, 0) + b'!', 'If-Match: "1"\r\n') + request('GET', 'q')) - expected = [(200, b'hello!'), (200, b'q1'), (200, b'q1'), (404, b''), (200, b'!'), (200, b'q1!')] + with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock: + reader = sock.makefile('rb') + for request in (edit_request('p', 0, 0, 0, 'hello'), edit_request('p', 1, 5, 0, '!'), + edit_request('p', 1, 0, 0, 'x'), view_request('body', 'p')): + sock.sendall(request) + answers.append(read_answer(reader)) + assert answers == [(200, 'none', b'hello'), (200, 'none', b'!'), (422, 'limit', b''), (200, 'none', b'hello!')], answers + pipeline = (view_request('body', 'p') + edit_request('q', 0, 0, 0, 'q1') + view_request('body', 'q') + + view_request('body', 'missing') + edit_request('q', 1, 2, 0, '!') + view_request('body', 'q')) + expected = [(200, 'none', b'hello!'), (200, 'none', b'q1'), (200, 'none', b'q1'), (404, 'no_value', b''), + (200, 'none', b'!'), (200, 'none', b'q1!')] for split in (len(pipeline), 7, 61): # one write, and small writes that cut heads and bodies with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock: for at in range(0, len(pipeline), split): sock.sendall(pipeline[at:at + split]) reader = sock.makefile('rb') - for status, body in expected: - code = int(reader.readline().split()[1]) - length = 0 - for line in iter(reader.readline, b'\r\n'): - name, _, value = line.decode().partition(':') - if name.lower() == 'content-length': - length = int(value) - assert (code, reader.read(length)) == (status, body), (split, code, status, body) - # The next round starts from the same documents. - host.request('PUT', 'q', struct.pack('<II', 0, 3) + b'q1', int(host.request('GET', 'q')[1].strip('"'))) - expected = [(200, b'hello!'), (412, b''), (200, b'q1'), (404, b''), (412, b''), (200, b'q1')] - pipeline = (request('GET', 'p') + request('PUT', 'q', struct.pack('<II', 0, 0) + b'q1', 'If-None-Match: *\r\n') + - request('GET', 'q') + request('GET', 'missing') + - request('PUT', 'q', struct.pack('<II', 2, 0) + b'!', 'If-Match: "1"\r\n') + request('GET', 'q')) + got = [read_answer(reader) for _ in expected] + assert got == expected, (split, got) + # The next round edits a document made already: its creation is refused, its later edit not. + revision = int(docs_call(host.port, view_request('revision', 'q'))[2]) + pipeline = (view_request('body', 'p') + edit_request('q', 0, 0, 0, 'q1') + view_request('size', 'q') + + view_request('body', 'missing') + edit_request('q', revision, 0, 0, '!') + view_request('size', 'q')) + size = int(docs_call(host.port, view_request('size', 'q'))[2]) + expected = [(200, 'none', b'hello!'), (422, 'limit', b''), (200, 'none', str(size).encode()), + (404, 'no_value', b''), (200, 'none', b'!'), (200, 'none', str(size + 1).encode())] finally: stats = host.stop() assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats return len(answers) + 3 * len(expected) -def server_lent_reads(aup, pure, work): - """A read lends the stored document's extents instead of copying them: a - 1 MiB answer sent slowly still carries the version read while the - document is replaced five times meanwhile.""" - import socket - import struct - module = work / 'editor-lent.pmod' - compile_module(aup, EDITOR, module) - host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'lent']) - big = bytes(range(256)) * 4096 - try: - assert host.request('PUT', 'big', struct.pack('<II', 0, 0) + big, 0)[0] == 200 - with socket.create_connection(('127.0.0.1', host.port), timeout=30) as sock: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096) - sock.sendall(b'GET /objects/big HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n') - data = sock.recv(100) - for revision in range(1, 6): - edit = struct.pack('<II', 0, len(big) if revision == 1 else 1) + b'Z' - assert host.request('PUT', 'big', edit, revision)[0] == 200 - while b'\r\n\r\n' not in data or len(data) < data.index(b'\r\n\r\n') + 4 + len(big): - chunk = sock.recv(65536) - assert chunk, len(data) - data += chunk - assert data[data.index(b'\r\n\r\n') + 4:] == big - assert host.request('GET', 'big') == (200, '"6"', 'none', b'Z') - finally: - stats = host.stop() - assert stats[0]['active'] == 0 and stats[0]['retained_payload_bytes'] == 0, stats - return len(big) - - def server_lent_budget(aup, pure, work): """Lent extents count against the payload budget while an answer waits - on its socket. Two readers pipeline 1 MiB reads without reading: the - kernel takes what it can and each reader leaves one answer waiting. A - third request finds no room (503 payload_budget) until a reader goes.""" + on its socket. Two readers pipeline 1 MiB reads of a text view without + reading: the kernel takes what it can and each reader leaves one answer + waiting. A third request finds no room (503 payload_budget) until a + reader goes.""" import socket - import struct import time - module = work / 'editor-budget.pmod' - compile_module(aup, EDITOR, module) - big = bytes(range(256)) * 4096 + module, source = work / 'note-budget.pmod', work / 'note.aup' + source.write_text(NOTE_SOURCE) + compile_module(aup, source, module) + big = (bytes(range(32, 127)) * ((1 << 20) // 95 + 1))[:1 << 20] # 1 MiB of text budget = 2 * len(big) + len(big) // 2 host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'lent-budget', '--budget', budget]) readers = [] try: - assert host.request('PUT', 'big', struct.pack('<II', 0, 0) + big, 0)[0] == 200 + assert post(host.port, '/inputs/write', {'text': big.decode()})[0] == 200 for _ in range(2): sock = socket.create_connection(('127.0.0.1', host.port), timeout=30) sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096) - sock.sendall(b'GET /objects/big HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n' * 32) + sock.sendall(b'GET /views/page HTTP/1.1\r\nHost: x\r\n\r\n' * 32) readers.append(sock) time.sleep(0.3) - refused = host.request('GET', 'big') - assert refused[0] == 503 and refused[2] == 'payload_budget', refused[:3] + refused = http(host.port, 'GET', '/views/page') + assert refused[:2] == (503, 'payload_budget'), refused[:2] readers.pop().close() for _ in range(100): - answered = host.request('GET', 'big') + answered = http(host.port, 'GET', '/views/page') if answered[0] == 200: break time.sleep(0.05) - assert answered[0] == 200 and answered[3] == big, answered[:3] + assert answered[0] == 200 and answered[3] == big, answered[:2] finally: for sock in readers: sock.close() @@ -351,25 +391,25 @@ def server_lent_views(aup, pure, work, threads=1): def server_bursts(aup, pure, work, clients=8, edits=30, shards=1): """Concurrent edits share commits, and every acknowledged edit survives a kill -9 of the server.""" - import http.client import signal - import struct import threading import time - module = work / 'editor-bursts.pmod' - compile_module(aup, EDITOR, module) + module = work / 'docs-bursts.pmod' + (work / 'docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'docs.aup', module) + store = work / f'bursts-{shards}' def load(host, rounds, acknowledged): def client(number): key, base = f'c{number}', acknowledged[number][1] for _ in range(rounds): try: - status, tag, _, _ = host.request('PUT', key, struct.pack('<II', 0, 0) + b'x', base) - except (OSError, http.client.HTTPException): # the server died mid-answer: not acknowledged + status, _, _ = docs_call(host.port, edit_request(key, base, 0, 0, 'x')) + except Exception: # the server died mid-answer: not acknowledged return if status != 200: return - base = int(tag.strip('"')) + base += 1 acknowledged[number] = (acknowledged[number][0] + 1, base) threads = [threading.Thread(target=client, args=(n,)) for n in range(clients)] start = time.perf_counter() @@ -377,22 +417,18 @@ def server_bursts(aup, pure, work, clients=8, edits=30, shards=1): thread.start() return threads, start - timings = {} - for name, args in [('program', [pure / 'prototypes/connections/build/tree/pure-connections', '--program', pure / 'prototypes/connections/examples/editor.pure']), - ('module', [pure / 'build/pure-serve', '--module', module, '--threads', str(shards)])]: - host = HttpHost(args + ['--store', work / f'bursts-{name}-{shards}']) - acknowledged = [(0, 0)] * clients - threads, start = load(host, edits, acknowledged) - for thread in threads: - thread.join() - timings[name] = time.perf_counter() - start - assert all(count == edits for count, _ in acknowledged), acknowledged - stats = host.stop() - if name == 'module': - bursts = stats[0]['bursts'] - assert stats[0]['burst_events'] == clients * edits and (clients == 1 or bursts < clients * edits), stats - # Kill the module server while clients edit; what it acknowledged is stored. - store = work / f'bursts-module-{shards}' + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)]) + acknowledged = [(0, 0)] * clients + threads, start = load(host, edits, acknowledged) + for thread in threads: + thread.join() + seconds = time.perf_counter() - start + assert all(count == edits for count, _ in acknowledged), acknowledged + stats = host.stop() + owner = next(line for line in stats if 'bursts' in line) + bursts = owner['bursts'] + assert owner['burst_events'] == clients * edits and (clients == 1 or bursts < clients * edits), stats + # Kill the server while clients edit; what it acknowledged is stored. host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)]) acknowledged = [(edits, edits)] * clients threads, _ = load(host, 1000, acknowledged) @@ -404,13 +440,13 @@ def server_bursts(aup, pure, work, clients=8, edits=30, shards=1): host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', str(shards)]) try: for number, (count, base) in enumerate(acknowledged): - status, tag, _, body = host.request('GET', f'c{number}') - stored = int(tag.strip('"')) - assert status == 200 and stored in (base, base + 1) and len(body) == stored, (number, count, base, tag) + status, _, body = docs_call(host.port, view_request('revision', f'c{number}')) + stored = int(body) + size = int(docs_call(host.port, view_request('size', f'c{number}'))[2]) + assert status == 200 and stored in (base, base + 1) and size == stored, (number, count, base, stored) finally: host.stop() - return {'writes': clients * edits, 'bursts': bursts, 'program_seconds': round(timings['program'], 3), - 'module_seconds': round(timings['module'], 3), + return {'writes': clients * edits, 'bursts': bursts, 'seconds': round(seconds, 3), 'acknowledged_before_kill': sum(count for count, _ in acknowledged) - clients * edits} @@ -1700,7 +1736,7 @@ def module_specs(aup, pure, work): import struct runs = {} program = work / 'sha256.pmod' - compile_module(aup, EDITOR.parent / 'sha256.aup', program) + compile_module(aup, EXAMPLES / 'sha256.aup', program) chooser = random.Random(140) messages = [b'abc', b'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq', b'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu'] @@ -1715,7 +1751,7 @@ def module_specs(aup, pure, work): assert digests == [hashlib.sha256(m).hexdigest() for m in messages], 'sha256' runs['sha256'] = len(messages) program = work / 'json.pmod' - compile_module(aup, EDITOR.parent / 'json.aup', program) + compile_module(aup, EXAMPLES / 'json.aup', program) suite = pure / 'tests/specs/json' cases = {name: (suite / name).read_bytes() for name in sorted(os.listdir(suite)) if name.endswith('.json')} cases['n_structure_100000_opening_arrays.json'] = b'[' * 100000 @@ -1776,7 +1812,7 @@ def module_specs(aup, pure, work): cases[f'r_{k}.json'] = json.dumps(value(0), ensure_ascii=chooser.random() < 0.5, indent=chooser.choice([None, 1, 2])).encode() (work / f'r_{k}.json').write_bytes(cases[f'r_{k}.json']) program = work / 'json_tree.pmod' - compile_module(aup, EDITOR.parent / 'json_tree.aup', program) + compile_module(aup, EXAMPLES / 'json_tree.aup', program) given = ''.join(f'message check @{work / name}\n' for name in cases) outputs = {name: subprocess.run([str(pure / 'build/pure-run'), str(program)], input=given, capture_output=True, text=True, timeout=600, env=dict(os.environ, **env)).stdout @@ -4445,12 +4481,12 @@ def cpu_seconds(pid): def server_clocks(aup, pure, work): """The wall clock moved an hour while a node runs (build/clock-back.so moves what the node reads of it, from its start): its timers, the pauses - of a wire whose receiver is down, the owner's waits and a store's group - commit all count on the monotonic clock. Were a wait's deadline read - from the wall clock, set back it would end at once, again and again (a - core spun, the wire tried thousands of times); set forward it would last - an hour (the timers stopped, the wire tried once, and a lone commit - after a group waited that long for the others to join it).""" + of a wire whose receiver is down and the owner's waits all count on the + monotonic clock. Were a wait's deadline read from the wall clock, set + back it would end at once, again and again (a core spun, the wire tried + thousands of times); set forward it would last an hour (the timers + stopped, the wire tried once, and a lone edit after three at once waited + that long).""" import os import socket import struct @@ -4479,12 +4515,12 @@ def server_clocks(aup, pure, work): (shift, stats['ticks'], share, stats['delivery_attempts']) seen[f'shift_{shift}'] = {'ticks': stats['ticks'], 'cpu_share': round(share, 3), 'wire_attempts': stats['delivery_attempts']} - editor = work / 'clocks-editor.pmod' - compile_module(aup, EDITOR, editor) + editor = work / 'clocks-docs.pmod' + (work / 'clocks-docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'clocks-docs.aup', editor) env = {**os.environ, 'LD_PRELOAD': str(pure / 'build/clock-back.so'), 'CLOCK_BACK_MS': '0', 'CLOCK_SHIFT_S': '3600'} host = HttpHost([pure / 'build/pure-serve', '--module', editor, '--store', work / 'clocks-docs', '--threads', 3], env=env) - new = struct.pack('<II', 0, 0) # an edit that makes the document: at 0, none removed, then its bytes statuses, alone = [], 0.0 try: for round_number in range(20): # three at once, grouped; then one alone @@ -4492,7 +4528,7 @@ def server_clocks(aup, pure, work): def put(k): together.wait() - statuses.append(host.request('PUT', f'd{round_number}-{k}', new + b'x' * 100000, 0)[0]) + statuses.append(docs_call(host.port, edit_request(f'd{round_number}-{k}', 0, 0, 0, 'x' * 100000))[0]) threads = [threading.Thread(target=put, args=(k,)) for k in range(3)] for thread in threads: @@ -4500,12 +4536,11 @@ def server_clocks(aup, pure, work): for thread in threads: thread.join() began = time.time() - statuses.append(host.request('PUT', f'alone-{round_number}', new + b'y', 0)[0]) + statuses.append(docs_call(host.port, edit_request(f'alone-{round_number}', 0, 0, 0, 'y'))[0]) alone = max(alone, time.time() - began) finally: - stats = host.stop()[-1] - assert statuses == [200] * 80 and alone < 1 and stats['grouped_commits'] > stats['commit_groups'], \ - (statuses, alone, stats) + host.stop() + assert statuses == [200] * 80 and alone < 1, (statuses, alone) seen['alone_after_groups_s'] = round(alone, 4) return seen @@ -5790,7 +5825,7 @@ def server_view_bodies(aup, pure, work): import random import threading module = work / 'routes-view.pmod' - compile_module(aup, EDITOR.parent / 'routes.aup', module) + compile_module(aup, EXAMPLES / 'routes.aup', module) rng = random.Random(18) n, edges = 300, set() while len(edges) < 1200: @@ -6268,8 +6303,9 @@ def server_tokens(aup, pure, work): for name, text in (('tokens', TOKENS_SOURCE), ('tokens-sender', TOKENS_SENDER)): (work / f'{name}.aup').write_text(text) compile_module(aup, work / f'{name}.aup', work / f'{name}.pmod') - editor = work / 'tokens-editor.pmod' - compile_module(aup, EDITOR, editor) + editor = work / 'tokens-docs.pmod' + (work / 'tokens-docs.aup').write_text(DOCS_SOURCE) + compile_module(aup, work / 'tokens-docs.aup', editor) serve = pure / 'build/pure-serve' names = {'read': 'r' * 40, 'write': 'w' * 40, 'input=add': 'i' * 40, 'read2': 'q' * 40} lengths = {n: bytes(65 + (k * 7) % 57 for k in range(n)).decode() for n in (1, 55, 56, 63, 64, 65, 119, 200)} @@ -6306,7 +6342,7 @@ def server_tokens(aup, pure, work): try: none = call('GET', '/t/views/total') assert none[:3] == (401, 'unauthorized', 'Bearer'), none - for method, path in (('POST', '/t/inputs/add?step=1'), ('GET', '/docs/objects/k'), ('PUT', '/t/queries/q'), + for method, path in (('POST', '/t/inputs/add?step=1'), ('GET', '/docs/views/body?id=k'), ('PUT', '/t/queries/q'), ('GET', '/t/queries/q')): assert call(method, path)[:2] == (401, 'unauthorized'), (method, path) assert call('GET', '/t/views/total', 'x' * 40)[:2] == (401, 'unauthorized') @@ -6314,11 +6350,11 @@ def server_tokens(aup, pure, work): assert call('GET', '/t/views/total', r)[0] == 200 assert call('GET', '/t/queries/q', r)[:2] == (404, 'no_value') # read, and there is none assert call('POST', '/t/inputs/add?step=1', r)[:2] == (403, 'forbidden') - assert call('PUT', '/docs/objects/k', r, b'x', {'If-None-Match': '*'})[:2] == (403, 'forbidden') + json_type = {'Content-Type': 'application/json'} + assert call('POST', '/docs/inputs/edit', r, edit_body('k', 0, 0, 0, 'x'), json_type)[:2] == (403, 'forbidden') assert call('POST', '/t/inputs/add?step=1', w)[0] == 200 - edit = struct.pack('<II', 0, 0) + b'hello' # the edit: start, remove, insert - assert call('PUT', '/docs/objects/k', w, edit, {'If-None-Match': '*'})[0] == 200 - assert call('GET', '/docs/objects/k', r)[::3] == (200, b'hello') + assert call('POST', '/docs/inputs/edit', w, edit_body('k', 0, 0, 0, 'hello'), json_type)[0] == 200 + assert call('GET', '/docs/views/body?id=k', r)[::3] == (200, b'hello') assert call('POST', '/t/inputs/add?step=2', i)[0] == 200 assert call('POST', '/t/inputs/other?step=1', i)[:2] == (403, 'forbidden') assert call('GET', '/t/views/total', i)[:2] == (403, 'forbidden') @@ -6417,14 +6453,14 @@ def server_tokens(aup, pure, work): def main(): - global EDITOR + global EXAMPLES parser = argparse.ArgumentParser() parser.add_argument('--aup', type=Path, required=True, help='the aup compiler') - parser.add_argument('--aup-root', type=Path, default=ROOT.parent / 'aup', help='its checkout, for examples/editor.aup') + parser.add_argument('--aup-root', type=Path, default=ROOT.parent / 'aup', help='its checkout, for its examples') parser.add_argument('--seeds', type=int, default=4) args = parser.parse_args() aup, pure = args.aup.resolve(), ROOT - EDITOR = args.aup_root.resolve() / 'examples' / 'editor.aup' + EXAMPLES = args.aup_root.resolve() / 'examples' report = {} with tempfile.TemporaryDirectory(prefix='pure-check-') as directory: work = Path(directory) @@ -6432,10 +6468,9 @@ def main(): report['module_assumptions'] = module_assumptions(aup, pure, work) report['module_specs'] = module_specs(aup, pure, work) report['module_tier'] = module_tier(aup, pure, work) - report['server_parity_requests'] = sum(server_parity(aup, pure, work, range(args.seeds), threads=threads) - for threads in (1, 3)) + report['server_documents_requests'] = sum(server_documents(aup, pure, work, range(args.seeds), threads=threads) + for threads in (1, 3)) report['server_pipelined_answers'] = server_pipelining(aup, pure, work) - report['server_lent_read_bytes'] = server_lent_reads(aup, pure, work) report['server_lent_budget_bytes'] = server_lent_budget(aup, pure, work) report['server_lent_views'] = [server_lent_views(aup, pure, work, threads) for threads in (1, 3)] report['server_bursts'] = server_bursts(aup, pure, work) |
