diff options
| author | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
| commit | b1ce1927022d374198441afafeb27199fc8a052a (patch) | |
| tree | 6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/connections/results/2026-09-23-document-edits | |
| parent | d34f9426d5317a1948f12f229439946728bc342a (diff) | |
| download | pure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz pure-b1ce1927022d374198441afafeb27199fc8a052a.zip | |
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/connections/results/2026-09-23-document-edits')
43 files changed, 6203 insertions, 0 deletions
diff --git a/prototypes/connections/results/2026-09-23-document-edits/README.md b/prototypes/connections/results/2026-09-23-document-edits/README.md new file mode 100644 index 0000000..ee6ed76 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/README.md @@ -0,0 +1,27 @@ +# 문서 부분 변경과 micro 비용 경계 + +2026-09-23, Linux AArch64 / GCC 14.2.0. 정확성·구조 비용 검증이며 성능 순위 자료가 아니다. + +- `native.json`, `sanitized.json`: 각 36개 통합 시나리오, 실행 코어와 splice 단위 검사, 바이너리·소스 해시. +- `core.json`: 실행 전용 아카이브의 섹션·심볼·구조체 비용과 검사 상한. +- `boundary-checks.json`: 코어만 선택한 빌드의 파일, 비용 증가 시 검사 실패, 전체 CLI/서버 ELF 비용. +- `verification.json`: 두 빌드의 소스와 바이너리 확인, 서버 종료 시 회수 확인. +- `sources/`: 해시가 일치하는 소스·기존 클라이언트 37개. 검증에 사용한 Workbench Wasm도 포함한다. + +실행 명령 (`prototypes/connections` 기준): + +```sh +make all +make check EVIDENCE=results/2026-09-23-document-edits/native.json +make check BUILD=build/sanitized CFLAGS='-std=c17 -O1 -g -Wall -Wextra -Wpedantic -Werror -fsanitize=address,undefined -fno-omit-frame-pointer' EVIDENCE=results/2026-09-23-document-edits/sanitized.json +make check-core CORE_EVIDENCE=results/2026-09-23-document-edits/core.json +``` + +기존 결과를 덮어쓰지 않는다. 재현 시 출력 인자를 생략하거나 새 경로를 사용한다. +동결 소스의 `sources/prototypes/connections`에서도 같은 빌드·검사가 가능하다. +C17 컴파일러·make·Python·Node·로컬 TCP/자식 프로세스 실행이 필요하며, 크기 검사는 +GNU 호환 ar/nm/size를 사용한다. ASan 계측물에는 release 크기 상한을 적용하지 않는다. + +1 MiB 문서 편집의 11 B는 입력 크레딧이다. 파일을 재작성하는 4 KiB 호스트 스택 버퍼, +커널·파일·allocator·파서·서버 비용을 포함한 전체 메모리라고 해석하지 않는다. +[구현, 수치 범위와 한계](../../../../docs/research/023-document-edits.md). diff --git a/prototypes/connections/results/2026-09-23-document-edits/boundary-checks.json b/prototypes/connections/results/2026-09-23-document-edits/boundary-checks.json new file mode 100644 index 0000000..c24fa21 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/boundary-checks.json @@ -0,0 +1,34 @@ +{ + "core_only_build_files": [ + "evaluate.o", + "libpure-execution.a", + "runtime.o" + ], + "reject_smaller_budget": [ + "domain_bytes: 200 > 199" + ], + "reject_added_dependency_and_static_state": [ + "unexpected archive members", + "bss: 4096 > 0", + "unexpected dependencies: ['malloc']" + ], + "selected_binaries": { + "pure-edit": { + "debug_file_bytes": 177384, + "stripped_file_bytes": 67904, + "sections": " text\t data\t bss\t dec\t hex\tfilename\n 35807\t 944\t 16\t 36767\t 8f9f\t/home/eunhhu/work/pure/prototypes/connections/build/pure-edit\n", + "direct_dynamic_libraries": [ + "0x0000000000000001 (NEEDED) Shared library: [libc.so.6]" + ] + }, + "pure-connections": { + "debug_file_bytes": 199080, + "stripped_file_bytes": 68000, + "sections": " text\t data\t bss\t dec\t hex\tfilename\n 42761\t 1040\t 16\t 43817\t ab29\t/home/eunhhu/work/pure/prototypes/connections/build/pure-connections\n", + "direct_dynamic_libraries": [ + "0x0000000000000001 (NEEDED) Shared library: [libc.so.6]" + ] + } + }, + "scope": "Selected files and direct ELF dependencies, not complete dependency memory, RSS, stack or latency." +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/core.json b/prototypes/connections/results/2026-09-23-document-edits/core.json new file mode 100644 index 0000000..8c0caf7 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/core.json @@ -0,0 +1,53 @@ +{ + "passed": true, + "failures": [], + "budget": { + "profile": "current-storage-execution-core", + "max_text_bytes": 6144, + "max_data_bytes": 0, + "max_bss_bytes": 0, + "layout_max_bytes": { + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680 + }, + "archive_members": [ + "runtime.o", + "evaluate.o" + ], + "external_symbols_allowed": [ + "memcpy", + "memmove", + "memset", + "strlen" + ], + "scope": "Release object sections and C bookkeeping, validated with GNU tools on Linux AArch64. Not total RAM, process RSS, stack, linked libraries, parser, storage, network or UI. 6 KiB is a regression ceiling for this profile, not a universal runtime size promise." + }, + "layout": { + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8 + }, + "sections": { + "text": 5932, + "data": 0, + "bss": 0 + }, + "members": [ + "runtime.o", + "evaluate.o" + ], + "external_symbols": [ + "memcpy", + "strlen" + ], + "size_output": " text\t data\t bss\t dec\t hex\tfilename\n 5012\t 0\t 0\t 5012\t 1394\truntime.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution.a)\n 920\t 0\t 0\t 920\t 398\tevaluate.o (ex /home/eunhhu/work/pure/prototypes/connections/build/libpure-execution.a)\n", + "archive_sha256": "d56c50e38c6c0949cfad069d34568da2ac45b1d60097cf9e7c4bf804f44c8128", + "layout_binary_sha256": "47cf1803007fc1ec85f119bd46195ad39d9f01ef0146bf5cc45e1af31eb18098", + "budget_sha256": "b4eceaffce900144470dd29076164039b8729bad4ba849789d212e7c952f5082", + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "size_tool": "GNU size (GNU Binutils for Debian) 2.44" +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/native.json b/prototypes/connections/results/2026-09-23-document-edits/native.json new file mode 100644 index 0000000..c9bb1d4 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/native.json @@ -0,0 +1,635 @@ +{ + "kind": "prepared-connection-correctness-not-performance", + "checks": [ + "fragmented request and binary bytes round trip", + "conditional create and stale revision preserve committed bytes", + "writer contention and invalid extent fail without overwriting data", + "two native writers cannot both create the same expected revision", + "incomplete request never commits and releases its allocation", + "borrowed read remains the same snapshot during another commit", + "receiver reset aborts an outstanding borrowed transfer", + "unsupported framing and oversized payload are rejected before effects", + "existing workbench manipulation restores in a fresh runtime and reports conflict", + "invalid or non-idle restore preserves local state and does not adopt remote revision", + "revision above JavaScript safe integer round trips without numeric coercion", + "unread acknowledgment does not undo commit or permit a stale retry", + "empty body has a valid lifetime and stored revision", + "committed authoring state survives process restart", + "slow consumer retains its lease; budget rejects another; deadline releases it", + "connection slots stay bounded and recover after header deadlines", + "authored nested read maps only final demand while preserving parent bounds and revision", + "unused checked view still fails even when response range would be valid", + "authored write and reply share selected input; invalid parent prevents the storage effect", + "body-free response preserves read existence and malformed-object errors", + "input-backed response survives partial send and releases on finish or abort without undoing commit", + "invalid authored program fails preparation before creating storage or accepting requests", + "text client uses character selection; named numeric fields select and persist the matching UTF-8 bytes", + "truncated layouts and invalid data-dependent ranges never change storage; empty and end selections remain valid", + "changed declaration order and unaligned numeric fields determine access; aliases add no dynamic steps", + "unused dynamic field selection retains its validation before the write effect", + "named variable tail reads use the same file version and charge only the selected suffix", + "data-dependent response keeps and charges the original input through partial transmission", + "native authored byte edit and fresh native process restore UTF-8 content", + "native stale revision and invalid byte position preserve the committed document", + "native convenience editor derives unaligned reordered field positions from the authored program", + "raw native apply runs another authored input contract without changing the executor", + "native CLI and external HTTP share revisions and the same authored storage effect in both directions", + "partial or invalid edit input never modifies the stored document", + "two processes cannot both splice the same expected document revision", + "1 MiB document changes with 11 input credits; larger input is rejected before an effect" + ], + "prepared_connections": { + "excerpt": { + "read": { + "to_end": false, + "offset": 32, + "length": 16, + "minimum_source_bytes": 80 + }, + "write": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "reply_input": false, + "view_definitions": 2, + "range_definitions": 2, + "runtime_view_steps": 0, + "write_step": 0, + "reply_step": 0, + "prepared_program_bytes": 680, + "steps": [], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + }, + "write": { + "read": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "to_end": false, + "offset": 5, + "length": 4, + "minimum_source_bytes": 12 + }, + "reply_input": true, + "view_definitions": 3, + "range_definitions": 2, + "runtime_view_steps": 0, + "write_step": 0, + "reply_step": 0, + "prepared_program_bytes": 680, + "steps": [], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + } + }, + "field_connections": { + "selection": { + "read": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "to_end": false, + "offset": 0, + "length": 0, + "minimum_source_bytes": 8 + }, + "reply_input": true, + "view_definitions": 2, + "range_definitions": 1, + "runtime_view_steps": 1, + "write_step": 1, + "reply_step": 1, + "prepared_program_bytes": 680, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 8, + "length": 0, + "to_end": true + }, + "offset": { + "value": 0, + "from_u32le": true + }, + "length": { + "value": 4, + "from_u32le": true + } + } + ], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + }, + "reordered": { + "read": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "to_end": false, + "offset": 0, + "length": 0, + "minimum_source_bytes": 11 + }, + "reply_input": true, + "view_definitions": 4, + "range_definitions": 1, + "runtime_view_steps": 1, + "write_step": 1, + "reply_step": 1, + "prepared_program_bytes": 680, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 11, + "length": 0, + "to_end": true + }, + "offset": { + "value": 7, + "from_u32le": true + }, + "length": { + "value": 3, + "from_u32le": true + } + } + ], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + }, + "field_get": { + "read": { + "to_end": true, + "offset": 11, + "length": 0, + "minimum_source_bytes": 11 + }, + "write": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "reply_input": false, + "view_definitions": 1, + "range_definitions": 0, + "runtime_view_steps": 0, + "write_step": 0, + "reply_step": 0, + "prepared_program_bytes": 680, + "steps": [], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + } + }, + "document_edits": { + "large_document_bytes": 1048576, + "small_edit_input_bytes": 11, + "storage_rewrites_full_extent": true, + "storage_scratch_bytes": 4096, + "kind": "correctness-and-admission-not-total-memory-or-performance" + }, + "server_runs": [ + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 6, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 0, + "gets": 0, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 41, + "commits": 12, + "gets": 14, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 31, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 2553, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 2, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2556, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 12, + "commits": 0, + "gets": 3, + "conflicts": 0, + "budget_rejections": 1, + "expired": 9, + "releases": 3, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048582, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 80, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 2, + "commits": 2, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 16, + "peak_connections": 1, + "accepted": 3, + "commits": 0, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 16, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 16, + "peak_connections": 1, + "accepted": 2, + "commits": 0, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 16, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 12, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 5, + "commits": 1, + "gets": 2, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 5, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 12, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 0, + "payload_budget": 1, + "peak_connections": 1, + "accepted": 3, + "commits": 0, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 0, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 0, + "budget_rejections": 2, + "expired": 0, + "releases": 5, + "ingress_prefix_copy_bytes": 8006, + "borrowed_output_bytes": 3145734, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 54, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 20, + "commits": 3, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 17, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 57, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 19, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 2, + "commits": 1, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 8, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 14, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 4, + "commits": 1, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 3, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 3, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 34, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 3, + "commits": 3, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 3, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 23, + "payload_budget": 23, + "peak_connections": 1, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 23, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 1048584, + "peak_connections": 2, + "accepted": 4, + "commits": 1, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 3, + "ingress_prefix_copy_bytes": 3998, + "borrowed_output_bytes": 2097091, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 9, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 0, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 2, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 114, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 11, + "payload_budget": 11, + "peak_connections": 1, + "accepted": 2, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 3, + "server_structure_bytes": 41528 + } + ], + "storage": "immutable-byte-extents-v1", + "execution_core": { + "runtime_tests": "passed", + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "forwarding_chain": 1024, + "native_storage_pipeline": true + }, + "execution_core_test_sha256": "07c328c98769d02d544aea2fc5389048a7620c00d9b3ea49b9c7ba45e33984b1", + "document_edit_units": { + "splice_tests": "passed", + "checks": [ + "authored-fields-and-atomic-parse-failure", + "binary-edit-oracle-and-old-snapshot", + "bounds-conflict-size-overflow", + "publication-failures-and-uncertain-durability", + "cancel-admission-and-borrowed-reply" + ] + }, + "document_edit_test_sha256": "0e4ec2c78334af2056af376a6ba9aef7416a34c6d4fbd9082f4abb640e6de28c", + "editor_sha256": "b5c6ec82a1e9c8a1391d1c4ac680e7f33e53dafe5c29ac8a956d5097653b1a03", + "binary_sha256": "e76d4c0a721c9ceb7142db0edbaa1f5c44bd78a2c55029658109c83e8076875f", + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "python": "3.14.7", + "node": "v24.20.0" + }, + "source_sha256": { + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6", + "prototypes/connections/test-runtime.c": "5d08639b5a71572553bda47cbd35562805d601aaa81f20731cd9154768ec025e", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "e1735316769b4ce4d5904b9587ccfa3ebeafa26517c53dba4491191d58c97b8e", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "793d0bde277563c9e11f1225804fb1646e4ce495979d16e8d70d00322a7bec7b", + "prototypes/connections/test-splice.c": "2415188c89ff413f10d55a8cf1902e76c585cc4102f01d504bbf84dbe8d90a8f", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "b5e7ed077be1333f3a948844608785dbab98bdc9c4ae6fe9c6a4805655fc9c63", + "prototypes/connections/core-layout.c": "e802b4976c53755ffc045ae9114ebdcf2989d526d8c871c3cfe89d057f4b82b0", + "prototypes/connections/check_core.py": "f80ead11764e870f0bde434c93233d518e5874fdb32f77444e7bd8bbe7398a10", + "prototypes/connections/core-budget.json": "b4eceaffce900144470dd29076164039b8729bad4ba849789d212e7c952f5082", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "prototypes/connections/examples/storage.pure": "6f2fd3733f46b762836405f40a9720de0b865f92a8bcbf9388dda0532c903c74", + "prototypes/connections/examples/excerpt.pure": "7ca1b9a4b6a06c32b95e37bcdeb2202ef912ffbf16163f7eea2b00574c703c16", + "prototypes/connections/examples/selection.pure": "90a19a080dd31b532163b8063057beee4701d8f74163ac4083833a1d664fe986", + "prototypes/connections/examples/select-text.py": "3211ce21bf6058f0131544959272587af4edb3fd5a956aee0a940ab19888f944", + "prototypes/workbench/remote.mjs": "6f5aa2bc5a3c88749b2b688c5631eed9e4a5ab41d72eca168bfb67941a8cff2f", + "prototypes/workbench/cli.mjs": "55a5ead56fc8ad4c3f2391505fc3dbfa477ccff5b68fc208f700da24d5e194da", + "prototypes/workbench/web/core.wasm": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a" + } +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sanitized.json b/prototypes/connections/results/2026-09-23-document-edits/sanitized.json new file mode 100644 index 0000000..0e2dbe4 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sanitized.json @@ -0,0 +1,635 @@ +{ + "kind": "prepared-connection-correctness-not-performance", + "checks": [ + "fragmented request and binary bytes round trip", + "conditional create and stale revision preserve committed bytes", + "writer contention and invalid extent fail without overwriting data", + "two native writers cannot both create the same expected revision", + "incomplete request never commits and releases its allocation", + "borrowed read remains the same snapshot during another commit", + "receiver reset aborts an outstanding borrowed transfer", + "unsupported framing and oversized payload are rejected before effects", + "existing workbench manipulation restores in a fresh runtime and reports conflict", + "invalid or non-idle restore preserves local state and does not adopt remote revision", + "revision above JavaScript safe integer round trips without numeric coercion", + "unread acknowledgment does not undo commit or permit a stale retry", + "empty body has a valid lifetime and stored revision", + "committed authoring state survives process restart", + "slow consumer retains its lease; budget rejects another; deadline releases it", + "connection slots stay bounded and recover after header deadlines", + "authored nested read maps only final demand while preserving parent bounds and revision", + "unused checked view still fails even when response range would be valid", + "authored write and reply share selected input; invalid parent prevents the storage effect", + "body-free response preserves read existence and malformed-object errors", + "input-backed response survives partial send and releases on finish or abort without undoing commit", + "invalid authored program fails preparation before creating storage or accepting requests", + "text client uses character selection; named numeric fields select and persist the matching UTF-8 bytes", + "truncated layouts and invalid data-dependent ranges never change storage; empty and end selections remain valid", + "changed declaration order and unaligned numeric fields determine access; aliases add no dynamic steps", + "unused dynamic field selection retains its validation before the write effect", + "named variable tail reads use the same file version and charge only the selected suffix", + "data-dependent response keeps and charges the original input through partial transmission", + "native authored byte edit and fresh native process restore UTF-8 content", + "native stale revision and invalid byte position preserve the committed document", + "native convenience editor derives unaligned reordered field positions from the authored program", + "raw native apply runs another authored input contract without changing the executor", + "native CLI and external HTTP share revisions and the same authored storage effect in both directions", + "partial or invalid edit input never modifies the stored document", + "two processes cannot both splice the same expected document revision", + "1 MiB document changes with 11 input credits; larger input is rejected before an effect" + ], + "prepared_connections": { + "excerpt": { + "read": { + "to_end": false, + "offset": 32, + "length": 16, + "minimum_source_bytes": 80 + }, + "write": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "reply_input": false, + "view_definitions": 2, + "range_definitions": 2, + "runtime_view_steps": 0, + "write_step": 0, + "reply_step": 0, + "prepared_program_bytes": 680, + "steps": [], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + }, + "write": { + "read": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "to_end": false, + "offset": 5, + "length": 4, + "minimum_source_bytes": 12 + }, + "reply_input": true, + "view_definitions": 3, + "range_definitions": 2, + "runtime_view_steps": 0, + "write_step": 0, + "reply_step": 0, + "prepared_program_bytes": 680, + "steps": [], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + } + }, + "field_connections": { + "selection": { + "read": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "to_end": false, + "offset": 0, + "length": 0, + "minimum_source_bytes": 8 + }, + "reply_input": true, + "view_definitions": 2, + "range_definitions": 1, + "runtime_view_steps": 1, + "write_step": 1, + "reply_step": 1, + "prepared_program_bytes": 680, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 8, + "length": 0, + "to_end": true + }, + "offset": { + "value": 0, + "from_u32le": true + }, + "length": { + "value": 4, + "from_u32le": true + } + } + ], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + }, + "reordered": { + "read": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "write": { + "to_end": false, + "offset": 0, + "length": 0, + "minimum_source_bytes": 11 + }, + "reply_input": true, + "view_definitions": 4, + "range_definitions": 1, + "runtime_view_steps": 1, + "write_step": 1, + "reply_step": 1, + "prepared_program_bytes": 680, + "steps": [ + { + "source_step": 0, + "source": { + "offset": 11, + "length": 0, + "to_end": true + }, + "offset": { + "value": 7, + "from_u32le": true + }, + "length": { + "value": 3, + "from_u32le": true + } + } + ], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + }, + "field_get": { + "read": { + "to_end": true, + "offset": 11, + "length": 0, + "minimum_source_bytes": 11 + }, + "write": { + "to_end": true, + "offset": 0, + "length": 0, + "minimum_source_bytes": 0 + }, + "reply_input": false, + "view_definitions": 1, + "range_definitions": 0, + "runtime_view_steps": 0, + "write_step": 0, + "reply_step": 0, + "prepared_program_bytes": 680, + "steps": [], + "write_effect": "write", + "splice_offset": { + "value": 0, + "from_u32le": false + }, + "splice_remove": { + "value": 0, + "from_u32le": false + } + } + }, + "document_edits": { + "large_document_bytes": 1048576, + "small_edit_input_bytes": 11, + "storage_rewrites_full_extent": true, + "storage_scratch_bytes": 4096, + "kind": "correctness-and-admission-not-total-memory-or-performance" + }, + "server_runs": [ + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 6, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 0, + "gets": 0, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048583, + "payload_budget": 4194304, + "peak_connections": 2, + "accepted": 41, + "commits": 12, + "gets": 14, + "conflicts": 4, + "budget_rejections": 0, + "expired": 0, + "releases": 31, + "ingress_prefix_copy_bytes": 11674, + "borrowed_output_bytes": 2115416, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 2553, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 2, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 2556, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 8, + "accepted": 12, + "commits": 0, + "gets": 3, + "conflicts": 0, + "budget_rejections": 1, + "expired": 9, + "releases": 3, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 1048582, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 80, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 2, + "commits": 2, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 16, + "peak_connections": 1, + "accepted": 3, + "commits": 0, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 16, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 16, + "payload_budget": 16, + "peak_connections": 1, + "accepted": 2, + "commits": 0, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 16, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 12, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 5, + "commits": 1, + "gets": 2, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 5, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 12, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 0, + "payload_budget": 1, + "peak_connections": 1, + "accepted": 3, + "commits": 0, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 0, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048576, + "payload_budget": 1048576, + "peak_connections": 2, + "accepted": 7, + "commits": 2, + "gets": 3, + "conflicts": 0, + "budget_rejections": 2, + "expired": 0, + "releases": 5, + "ingress_prefix_copy_bytes": 8006, + "borrowed_output_bytes": 3145734, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 54, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 20, + "commits": 3, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 17, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 57, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 19, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 2, + "commits": 1, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 2, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 8, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 14, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 4, + "commits": 1, + "gets": 1, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 3, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 3, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 34, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 3, + "commits": 3, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 3, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 23, + "payload_budget": 23, + "peak_connections": 1, + "accepted": 3, + "commits": 0, + "gets": 2, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 23, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 1048584, + "payload_budget": 1048584, + "peak_connections": 2, + "accepted": 4, + "commits": 1, + "gets": 2, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 3, + "ingress_prefix_copy_bytes": 3998, + "borrowed_output_bytes": 2097091, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 9, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 1, + "commits": 0, + "gets": 0, + "conflicts": 0, + "budget_rejections": 0, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 0, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 29, + "payload_budget": 4194304, + "peak_connections": 1, + "accepted": 10, + "commits": 2, + "gets": 4, + "conflicts": 1, + "budget_rejections": 0, + "expired": 0, + "releases": 10, + "ingress_prefix_copy_bytes": 7, + "borrowed_output_bytes": 114, + "server_structure_bytes": 41528 + }, + { + "active": 0, + "retained_payload_bytes": 0, + "peak_retained_payload_bytes": 11, + "payload_budget": 11, + "peak_connections": 1, + "accepted": 2, + "commits": 1, + "gets": 0, + "conflicts": 0, + "budget_rejections": 1, + "expired": 0, + "releases": 1, + "ingress_prefix_copy_bytes": 0, + "borrowed_output_bytes": 3, + "server_structure_bytes": 41528 + } + ], + "storage": "immutable-byte-extents-v1", + "execution_core": { + "runtime_tests": "passed", + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "forwarding_chain": 1024, + "native_storage_pipeline": true + }, + "execution_core_test_sha256": "776be59b5e35de39587fd5a8f703160572b3a6c098a22af635e597208bba1950", + "document_edit_units": { + "splice_tests": "passed", + "checks": [ + "authored-fields-and-atomic-parse-failure", + "binary-edit-oracle-and-old-snapshot", + "bounds-conflict-size-overflow", + "publication-failures-and-uncertain-durability", + "cancel-admission-and-borrowed-reply" + ] + }, + "document_edit_test_sha256": "ec0db33c9937034148d934c658c98aa98a2fdb279a92f96a2d7374e2d292468e", + "editor_sha256": "23ee47873d7924163ace86cad44946e71fd21a6122930f35eff1c36f5e418bdc", + "binary_sha256": "f145f0f30c944e400ef8d1594bd0387f42f42569290f28c7a1272a4ee8849d4c", + "environment": { + "platform": "Linux-6.18.39+rpt-rpi-2712-aarch64-with-glibc2.41", + "machine": "aarch64", + "python": "3.14.7", + "node": "v24.20.0" + }, + "source_sha256": { + "prototypes/connections/flow.c": "3957de06e33985a8310954af07b33c2a1d02a4afdf9bd112d06c99c0b5df7749", + "prototypes/connections/flow.h": "289ff43d191270066e238235023f096946f3f561ec5825d2f6c26006db5fc421", + "prototypes/connections/store.c": "44702e9414c26d1ee336b2e36e6713560013c2c8d0091a8900042e2468f88015", + "prototypes/connections/store.h": "e3cf5f169be02e40f1274105046f29a63a46cd153beaf204d33d6100b9c869b8", + "prototypes/connections/program.c": "d7186634d2bfd83bbaf87ea3bafdec8d14be8f556cbf3d9877d07e0074b4b389", + "prototypes/connections/program.h": "b9ea0af3759150637ccba9362ccb73be374ed85b449b0461fc61fb14ee50e8bc", + "prototypes/connections/runtime.c": "a6c068c6a2cee214b3fe30241459394833917dfb6c0d33052460eab4c6a539a2", + "prototypes/connections/runtime.h": "ff03c0be92dcc482247a58a0be54d2dc5c270325b51ceeed37afc2ffa5480b1f", + "prototypes/connections/result.h": "00b6cf02262039fd90b197eb781f38c9771beaa8d65f394cba6d3f8d0b2ae79d", + "prototypes/connections/evaluate.c": "ee8b91efc733b9d305711b7cbb7b502ae5d3917e06f750b7f62b632ed0ea6d04", + "prototypes/connections/store-port.c": "d6bf17878b325e039360816edcbf8d68e41b373cf3e6128d7795e11ae21fdd6f", + "prototypes/connections/store-port.h": "a262bac93463bfe9939ca0bdc5989d1665aafc007d61e93c28ffc36478b1fde6", + "prototypes/connections/test-runtime.c": "5d08639b5a71572553bda47cbd35562805d601aaa81f20731cd9154768ec025e", + "prototypes/connections/server.c": "76645bafe8e4e429155efe96c2d05a59aed506ba89736283e4fdae71a3d9a60f", + "prototypes/connections/check.py": "e1735316769b4ce4d5904b9587ccfa3ebeafa26517c53dba4491191d58c97b8e", + "prototypes/connections/check_edits.py": "7699eb4bda57ad860acf7be6da3e6de56d6b9a3dcf3b9ff9c7df104f5bcb9224", + "prototypes/connections/edit.c": "793d0bde277563c9e11f1225804fb1646e4ce495979d16e8d70d00322a7bec7b", + "prototypes/connections/test-splice.c": "2415188c89ff413f10d55a8cf1902e76c585cc4102f01d504bbf84dbe8d90a8f", + "prototypes/connections/test-flow.c": "2cd2618015ba6806b1a733af0fc7d0ed6aec48fdec117d18c3a8160f534e130b", + "prototypes/connections/test-program.c": "a1b65be05ab2e4e5ccbc56cd1e03e17b0d885f15dc027bf295371ec821e19cc8", + "prototypes/connections/test-fields.c": "e5856ae1985dc8407565ebf7b65702478a34a05081c955f127a10d3cdc900507", + "prototypes/connections/Makefile": "b5e7ed077be1333f3a948844608785dbab98bdc9c4ae6fe9c6a4805655fc9c63", + "prototypes/connections/core-layout.c": "e802b4976c53755ffc045ae9114ebdcf2989d526d8c871c3cfe89d057f4b82b0", + "prototypes/connections/check_core.py": "f80ead11764e870f0bde434c93233d518e5874fdb32f77444e7bd8bbe7398a10", + "prototypes/connections/core-budget.json": "b4eceaffce900144470dd29076164039b8729bad4ba849789d212e7c952f5082", + "prototypes/connections/examples/editor.pure": "8ca6c191b98bf53a592ce24f2b5cd040e41f05c4ccdda7a072d5d7f79870c06a", + "prototypes/connections/examples/storage.pure": "6f2fd3733f46b762836405f40a9720de0b865f92a8bcbf9388dda0532c903c74", + "prototypes/connections/examples/excerpt.pure": "7ca1b9a4b6a06c32b95e37bcdeb2202ef912ffbf16163f7eea2b00574c703c16", + "prototypes/connections/examples/selection.pure": "90a19a080dd31b532163b8063057beee4701d8f74163ac4083833a1d664fe986", + "prototypes/connections/examples/select-text.py": "3211ce21bf6058f0131544959272587af4edb3fd5a956aee0a940ab19888f944", + "prototypes/workbench/remote.mjs": "6f5aa2bc5a3c88749b2b688c5631eed9e4a5ab41d72eca168bfb67941a8cff2f", + "prototypes/workbench/cli.mjs": "55a5ead56fc8ad4c3f2391505fc3dbfa477ccff5b68fc208f700da24d5e194da", + "prototypes/workbench/web/core.wasm": "9f760e4a2ce6ea11f2be07693fceca74088884885cc545a70ceef5003d0b2479", + "prototypes/workbench/web/runtime.mjs": "9cf12270bb39b3d6ce0384c56df8bdfabfd9459d3375cc7819290a5fe3e5db54", + "prototypes/workbench/web/compiler.mjs": "349139612d1f7da5f75ef21c65aa697963b760cb35321150fc36f67f3b40adf1", + "prototypes/workbench/web/project.mjs": "8af4cd1a60bbb217c896be20c7c6b8d33989f2166d06e736bf1479d8ecdc8b35", + "prototypes/workbench/examples/direct.pure": "57177e5197d75a6185cd3ac5df2be920ddba9a9b0fcc2dddab3560b815249f0a" + } +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/Makefile b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/Makefile new file mode 100644 index 0000000..d1b46e2 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/Makefile @@ -0,0 +1,59 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build + +.PHONY: all core editor server check check-core +CORE = runtime.c evaluate.c +HOST = store.c store-port.c +HEADERS = runtime.h program.h store.h store-port.h flow.h result.h +all: core editor server +core: $(BUILD)/libpure-execution.a +editor: $(BUILD)/pure-edit +server: $(BUILD)/pure-connections + +$(BUILD): + mkdir -p $@ + +$(BUILD)/pure-connections: server.c $(HOST) flow.c program.c $(CORE) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) server.c $(HOST) flow.c program.c $(CORE) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/pure-edit: edit.c $(HOST) program.c $(CORE) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) edit.c $(HOST) program.c $(CORE) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/runtime.o: runtime.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -c runtime.c -o $@ + +$(BUILD)/evaluate.o: evaluate.c program.h flow.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -c evaluate.c -o $@ + +$(BUILD)/libpure-execution.a: $(BUILD)/runtime.o $(BUILD)/evaluate.o + $(AR) rcs $@ $^ + +$(BUILD)/test-flow: test-flow.c store.c flow.c store.h flow.h result.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-flow.c store.c flow.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-program: test-program.c program.c evaluate.c program.h store.h flow.h result.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-program.c program.c evaluate.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-fields: test-fields.c program.c evaluate.c program.h store.h flow.h result.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-fields.c program.c evaluate.c $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-runtime: test-runtime.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) test-runtime.c program.c $(CORE) $(HOST) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-splice.c program.c $(CORE) $(HOST) $(LDFLAGS) $(LDLIBS) -o $@ + +$(BUILD)/core-layout: core-layout.c $(HEADERS) | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) core-layout.c $(LDFLAGS) $(LDLIBS) -o $@ + +# Run on the release build, separately from sanitizer-instrumented objects. +check-core: core $(BUILD)/core-layout + python3 check_core.py --archive $(BUILD)/libpure-execution.a --layout $(BUILD)/core-layout $(if $(CORE_EVIDENCE),--output "$(CORE_EVIDENCE)") + +check: $(BUILD)/test-flow $(BUILD)/test-program $(BUILD)/test-fields $(BUILD)/test-runtime $(BUILD)/test-splice $(BUILD)/pure-edit $(BUILD)/pure-connections + $(BUILD)/test-flow + $(BUILD)/test-program + $(BUILD)/test-fields + python3 check.py --server $(BUILD)/pure-connections --runtime-test $(BUILD)/test-runtime --splice-test $(BUILD)/test-splice --editor $(BUILD)/pure-edit $(if $(EVIDENCE),--output "$(EVIDENCE)") diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check.py b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check.py new file mode 100644 index 0000000..4538995 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check.py @@ -0,0 +1,551 @@ +#!/usr/bin/env python3 +"""Exercise local TCP, immutable byte extents, and the existing workbench CLI.""" +import argparse +from contextlib import contextmanager +from concurrent.futures import ThreadPoolExecutor +import fcntl +import hashlib +import http.client +import json +import platform +from pathlib import Path +import select +import socket +import struct +import subprocess +import sys +import tempfile +import time +from check_edits import check_edits + +ROOT = Path(__file__).resolve().parent +WORKBENCH = ROOT.parent / 'workbench' +OBJECT_LIMIT = (2 * 6 + 32) * 32768 + 8192 + + +class Server: + def __init__(self, binary, storage, budget=4 * 1024 * 1024, timeout=5000, program=None): + self.process = subprocess.Popen([str(binary), '--store', str(storage), '--port', '0', + '--program', str(program or ROOT / 'examples/storage.pure'), + '--budget', str(budget), '--timeout-ms', str(timeout), '--send-buffer', '4096'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if not select.select([self.process.stdout], [], [], 15)[0]: + self.process.kill() + raise AssertionError('server readiness timed out') + line = self.process.stdout.readline() + if not line: + raise AssertionError(self.process.communicate(timeout=5)) + self.ready = json.loads(line) + self.port = self.ready['port'] + self.stats = None + + def close(self): + if self.process.poll() is None: + self.process.terminate() + try: + stdout, stderr = self.process.communicate(timeout=10) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.communicate() + raise + assert self.process.returncode == 0, (stdout, stderr) + self.stats = json.loads(stderr) + assert self.stats['active'] == self.stats['retained_payload_bytes'] == 0 + assert self.stats['peak_retained_payload_bytes'] <= self.stats['payload_budget'] + assert self.stats['peak_connections'] <= 8 + + +@contextmanager +def serving(*args, **kwargs): + server = Server(*args, **kwargs) + try: + yield server + finally: + server.close() + + +def rpc(server, method, key, body=b'', expected=None): + 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', server.port, timeout=8) + try: + connection.request(method, f'/objects/{key}', body, headers) + response = connection.getresponse() + return response.status, dict(response.getheaders()), response.read() + finally: + connection.close() + + +def wire(server, request, chunks=None): + with socket.create_connection(('127.0.0.1', server.port), timeout=8) as sock: + if chunks: + for chunk in chunks: + sock.sendall(chunk) + else: + sock.sendall(request) + response = http.client.HTTPResponse(sock) + response.begin() + return response.status, dict(response.getheaders()), response.read() + + +def header(method, key, size=0, condition=b''): + return (f'{method} /objects/{key} HTTP/1.1\r\nHost: localhost\r\n' + f'Content-Length: {size}\r\n'.encode() + condition + b'\r\n') + + +def cli(server, commands, source=None, expected_code=0): + result = subprocess.run(['node', str(WORKBENCH / 'cli.mjs'), *([str(source)] if source else [])], + input=commands.replace('$URL', f'http://127.0.0.1:{server.port}'), text=True, + capture_output=True, timeout=30) + assert result.returncode == expected_code, (result.stdout, result.stderr) + return [json.loads(line) for line in result.stdout.splitlines()], result.stderr + + +def check(binary, directory, editor=None): + storage = directory / 'objects' + checks, records = [], [] + with serving(binary, storage) as server: + assert rpc(server, 'GET', 'absent')[0] == 404 + payload = b'\x00\xffbinary\r\n\r\n' + bytes(range(256)) * 41 + request = header('PUT', 'binary', len(payload), b'If-None-Match: *\r\n') + payload + status, headers, _ = wire(server, request, [request[:7], request[7:51], request[51:57], request[57:]]) + assert status == 200 and headers['ETag'] == '"1"' + assert rpc(server, 'GET', 'binary')[2] == payload + checks.append('fragmented request and binary bytes round trip') + assert rpc(server, 'PUT', 'binary', b'new', 0)[0] == 412 + assert rpc(server, 'PUT', 'binary', b'new', 1)[0] == 200 + assert rpc(server, 'PUT', 'binary', b'stale', 1)[0] == 412 + assert rpc(server, 'GET', 'binary')[2] == b'new' + checks.append('conditional create and stale revision preserve committed bytes') + + with (storage / '.writer.lock').open('r+b') as lock: + fcntl.lockf(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + assert rpc(server, 'PUT', 'binary', b'blocked', 2)[0] == 503 + fcntl.lockf(lock, fcntl.LOCK_UN) + broken = storage / 'obj-broken' + broken.write_bytes(b'incomplete') + assert rpc(server, 'GET', 'broken')[0] == 500 + assert rpc(server, 'PUT', 'broken', b'failure', 0)[0] == 500 + assert broken.read_bytes() == b'incomplete' + assert rpc(server, 'GET', 'binary')[2] == b'new' + checks.append('writer contention and invalid extent fail without overwriting data') + + with serving(binary, storage) as second_writer: + with ThreadPoolExecutor(max_workers=2) as pool: + tasks = [pool.submit(rpc, target, 'PUT', 'race', value, 0) + for target, value in [(server, b'first'), (second_writer, b'second')]] + statuses = [task.result()[0] for task in tasks] + assert statuses.count(200) == 1 and all(status in [200, 412, 503] for status in statuses) + status, headers, body = rpc(server, 'GET', 'race') + assert status == 200 and headers['ETag'] == '"1"' and body in [b'first', b'second'] + records.append(second_writer.stats) + checks.append('two native writers cannot both create the same expected revision') + + sock = socket.create_connection(('127.0.0.1', server.port), timeout=8) + sock.sendall(header('PUT', 'incomplete', 100, b'If-None-Match: *\r\n') + b'partial') + sock.shutdown(socket.SHUT_WR) + assert sock.recv(1) == b'' + sock.close() + assert rpc(server, 'GET', 'incomplete')[0] == 404 + checks.append('incomplete request never commits and releases its allocation') + + # Force genuine partial socket sends. A write on another connection must + # not alter the immutable file whose bytes are still being transmitted. + large = bytes(range(256)) * 4096 + assert rpc(server, 'PUT', 'large', large, 0)[0] == 200 + sock = socket.create_connection(('127.0.0.1', server.port), timeout=8) + sock.sendall(header('GET', 'large')) + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == 200 and response.getheader('ETag') == '"1"' + assert response.read(17) == large[:17] + assert rpc(server, 'PUT', 'large', b'changed', 1)[0] == 200 + assert response.read() == large[17:] + sock.close() + assert rpc(server, 'GET', 'large')[2] == b'changed' + checks.append('borrowed read remains the same snapshot during another commit') + + assert rpc(server, 'PUT', 'large', large, 2)[0] == 200 + sock = socket.create_connection(('127.0.0.1', server.port), timeout=8) + sock.sendall(header('GET', 'large')) + assert sock.recv(1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0)) + sock.close() + assert rpc(server, 'GET', 'binary')[2] == b'new' + checks.append('receiver reset aborts an outstanding borrowed transfer') + + assert wire(server, header('PUT', 'too_large', OBJECT_LIMIT + 1, b'If-None-Match: *\r\n'))[0] == 413 + malformed = [ + b'PUT /objects/bad HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\nContent-Length: 1\r\nIf-None-Match: *\r\n\r\nx', + b'PUT /objects/bad HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n', + b'GET /objects/bad HTTP/1.1\r\nHost: x\x00hidden\r\n\r\n', + ] + for request in malformed: + assert wire(server, request)[0] == 400 + assert rpc(server, 'GET', 'bad')[0] == 404 + checks.append('unsupported framing and oversized payload are rejected before effects') + + # Existing authoring path: define, manipulate, save remotely, reconstruct + # in a fresh runtime. No fabricated project fixture or mock storage. + source = WORKBENCH / 'examples' / 'direct.pure' + saved = directory / 'restored.json' + lines, _ = cli(server, 'point drag down 80 180\npoint drag move 210 210\npoint drag up 210 210\n' + 'connect $URL authored\npush 0\nquit\n', source) + snapshots = [line for line in lines if 'states' in line] + assert snapshots + expected_states = snapshots[-1]['states'] + assert expected_states['x'] == 210 and expected_states['y'] == 190 and expected_states['dragging'] == 0 + lines, _ = cli(server, f'connect $URL authored\npull\nsave-project {saved}\nquit\n') + assert [line for line in lines if 'states' in line][-1]['states'] == expected_states + document = json.loads(saved.read_text()) + assert document['version'] == 2 and document['source'] == source.read_text() + lines, error = cli(server, 'connect $URL authored\npush 0\nquit\n', source, 1) + assert '412' in error + checks.append('existing workbench manipulation restores in a fresh runtime and reports conflict') + + assert rpc(server, 'PUT', 'invalid_project', b'{"format":"pure-workbench","version":2,"source":"state x=1","states":[]}', 0)[0] == 200 + lines, error = cli(server, 'show\nconnect $URL invalid_project\npull\nshow\nquit\n', source, 1) + states = [line['states'] for line in lines if 'states' in line] + assert states[0] == states[-1] and error + lines, error = cli(server, 'point drag down 90 170\nconnect $URL authored\npull\npush\nshow\nquit\n', source, 1) + states = [line['states'] for line in lines if 'states' in line] + assert states[0] == states[-1] and states[-1]['dragging'] == 1 + assert len(error.strip().splitlines()) == 2 + checks.append('invalid or non-idle restore preserves local state and does not adopt remote revision') + + assert rpc(server, 'PUT', 'wide_revision', rpc_bytes(saved), 0)[0] == 200 + wide = storage / 'obj-wide_revision' + raw = wide.read_bytes() + wide.write_bytes(raw[:8] + struct.pack('<Q', 9007199254740993) + raw[16:]) + lines, _ = cli(server, 'connect $URL wide_revision\npull\npush\nquit\n') + revisions = [line['revision'] for line in lines if 'remote' in line and 'revision' in line] + assert revisions == ['9007199254740993', '9007199254740994'] + checks.append('revision above JavaScript safe integer round trips without numeric coercion') + + sock = socket.create_connection(('127.0.0.1', server.port), timeout=8) + sock.sendall(header('PUT', 'unread_ack', 4, b'If-None-Match: *\r\n') + b'done') + try: + deadline = time.monotonic() + 3 + while True: + committed = storage / 'obj-unread_ack' + if committed.exists(): + assert committed.read_bytes()[24:] == b'done' + break + assert time.monotonic() < deadline + time.sleep(0.01) + finally: + sock.close() # Client never consumes the successful response. + assert rpc(server, 'PUT', 'unread_ack', b'again', 0)[0] == 412 + assert rpc(server, 'GET', 'unread_ack')[2] == b'done' + checks.append('unread acknowledgment does not undo commit or permit a stale retry') + + # A disconnect after commit can hide the ACK; retries remain conditional. + assert rpc(server, 'PUT', 'empty', b'', 0)[0] == 200 + assert rpc(server, 'GET', 'empty')[2] == b'' + checks.append('empty body has a valid lifetime and stored revision') + records.append(server.stats) + + with serving(binary, storage) as restarted: + assert rpc(restarted, 'GET', 'binary')[2] == b'new' + assert rpc(restarted, 'GET', 'authored')[2] == rpc_bytes(saved) + checks.append('committed authoring state survives process restart') + records.append(restarted.stats) + + # Fixed small payload budget is an admission test, not a throughput workload. + with serving(binary, storage, budget=1024 * 1024, timeout=1200) as limited: + sock = socket.create_connection(('127.0.0.1', limited.port), timeout=8) + sock.sendall(header('GET', 'large')) + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == 200 + assert response.read(1) == large[:1] + status, headers, _ = rpc(limited, 'GET', 'large') + assert status == 503 and headers['X-Pure-Error'] == 'payload_budget' + # Leave the first receiver stalled until the absolute connection deadline. + time.sleep(1.4) + assert rpc(limited, 'GET', 'binary')[2] == b'new' + response.close() + sock.close() + checks.append('slow consumer retains its lease; budget rejects another; deadline releases it') + + idle = [socket.create_connection(('127.0.0.1', limited.port), timeout=8) for _ in range(8)] + try: + for s in idle: + s.sendall(b'G') + for s in idle: + assert s.recv(1) == b'' + finally: + for s in idle: + s.close() + assert rpc(limited, 'GET', 'binary')[0] == 200 + checks.append('connection slots stay bounded and recover after header deadlines') + assert limited.stats['budget_rejections'] >= 1 and limited.stats['expired'] >= 9 + records.append(limited.stats) + prepared = authored_connections(binary, directory, storage, records, checks) + fields = field_connections(binary, directory, storage, records, checks) + edits = check_edits(editor, binary, ROOT, directory, serving, rpc, header, records, checks) if editor else None + return {'kind': 'prepared-connection-correctness-not-performance', 'checks': checks, + 'prepared_connections': prepared, + 'field_connections': fields, + 'document_edits': edits, + 'server_runs': records, 'storage': server.ready['storage']} + + +def authored_connections(binary, directory, storage, records, checks): + get = 'on get { let o = store.read(request.key); reply(o, o.body); }' + put = 'on put { let s = store.write(request.key, request.revision, request.body); reply(s); }' + + def author(name, source): + path = directory / name + path.write_text(source) + return path + + def explain(path): + return json.loads(subprocess.check_output([str(binary), '--check-program', str(path)], text=True)) + + with serving(binary, storage) as writer: + assert rpc(writer, 'PUT', 'short_parent', bytes(range(48)), 0)[0] == 200 + assert rpc(writer, 'PUT', 'short_unused', bytes(range(80)), 0)[0] == 200 + records.append(writer.stats) + excerpt = ROOT / 'examples/excerpt.pure' + prepared = {'excerpt': explain(excerpt)} + assert prepared['excerpt']['read'] == { + 'to_end': False, 'offset': 32, 'length': 16, 'minimum_source_bytes': 80} + with serving(binary, storage, budget=16, program=excerpt) as selected: + status, headers, body = rpc(selected, 'GET', 'large') + assert status == 200 and body == bytes(range(32, 48)) and headers['ETag'] == '"3"' + # Final [32,48) fits, but the authored parent [16,80) does not. + assert rpc(selected, 'GET', 'short_parent')[0] == 413 + assert rpc(selected, 'GET', 'missing')[0] == 404 + assert selected.stats['peak_retained_payload_bytes'] == 16 + records.append(selected.stats) + checks.append('authored nested read maps only final demand while preserving parent bounds and revision') + + unused = author('unused.pure', 'on get { let o = store.read(request.key); ' + 'let unused = range(o.body, 0, 96); let body = range(o.body, 32, 16); reply(o, body); } ' + put) + with serving(binary, storage, budget=16, program=unused) as guarded: + assert rpc(guarded, 'GET', 'short_unused')[0] == 413 + assert rpc(guarded, 'GET', 'large')[2] == bytes(range(32, 48)) + records.append(guarded.stats) + checks.append('unused checked view still fails even when response range would be valid') + + select_write = author('write.pure', get + ' on put { let a = range(request.body, 2, 10); ' + 'let b = range(a, 3, 4); let alias = b; ' + 'let saved = store.write(request.key, request.revision, alias); reply(saved, b); }') + prepared['write'] = explain(select_write) + with serving(binary, storage, program=select_write) as writer: + status, headers, body = rpc(writer, 'PUT', 'selected', b'0123456789AB', 0) + assert status == 200 and body == b'5678' and headers['ETag'] == '"1"' + assert rpc(writer, 'GET', 'selected')[2] == b'5678' + assert rpc(writer, 'PUT', 'selected', b'012345678', 1)[0] == 413 + status, headers, body = rpc(writer, 'GET', 'selected') + assert status == 200 and body == b'5678' and headers['ETag'] == '"1"' + assert rpc(writer, 'PUT', 'selected', b'0123456789AB', 0)[0] == 412 + records.append(writer.stats) + checks.append('authored write and reply share selected input; invalid parent prevents the storage effect') + + metadata = author('metadata.pure', 'on get { let o = store.read(request.key); reply(o); } ' + put) + with serving(binary, storage, budget=1, program=metadata) as meta: + status, headers, body = rpc(meta, 'GET', 'large') + assert status == 200 and headers['ETag'] == '"3"' and body == b'' + assert rpc(meta, 'GET', 'absent')[0] == 404 + assert rpc(meta, 'GET', 'broken')[0] == 500 + assert meta.stats['peak_retained_payload_bytes'] == 0 + records.append(meta.stats) + checks.append('body-free response preserves read existence and malformed-object errors') + + echo = author('echo.pure', get + ' on put { let body = request.body; ' + 'let saved = store.write(request.key, request.revision, body); reply(saved, body); }') + payload = bytes(range(256)) * 4096 + with serving(binary, storage, budget=len(payload), program=echo) as echoed: + with socket.create_connection(('127.0.0.1', echoed.port), timeout=8) as sock: + sock.sendall(header('PUT', 'echo', len(payload), b'If-None-Match: *\r\n') + payload) + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == 200 and response.read(1) == payload[:1] + assert rpc(echoed, 'GET', 'binary')[0] == 503 + assert response.read() == payload[1:] + response.close() + assert rpc(echoed, 'GET', 'binary')[2] == b'new' + with socket.create_connection(('127.0.0.1', echoed.port), timeout=8) as sock: + sock.sendall(header('PUT', 'echo_abort', len(payload), b'If-None-Match: *\r\n') + payload) + assert sock.recv(1) + assert rpc(echoed, 'GET', 'binary')[0] == 503 + sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0)) + deadline = time.monotonic() + 3 + while rpc(echoed, 'GET', 'binary')[0] == 503: + assert time.monotonic() < deadline + time.sleep(0.01) + assert rpc(echoed, 'GET', 'echo_abort')[2] == payload + records.append(echoed.stats) + checks.append('input-backed response survives partial send and releases on finish or abort without undoing commit') + + bad = author('invalid.pure', get + get + put) + untouched = directory / 'must_not_exist' + rejected = subprocess.run([str(binary), '--program', str(bad), '--store', str(untouched)], + capture_output=True, text=True, timeout=10) + assert rejected.returncode == 2 and 'duplicate handler' in rejected.stderr and not untouched.exists() + checks.append('invalid authored program fails preparation before creating storage or accepting requests') + return prepared + + +def field_connections(binary, directory, storage, records, checks): + program = ROOT / 'examples/selection.pure' + plans = {'selection': json.loads(subprocess.check_output( + [str(binary), '--check-program', str(program)], text=True))} + assert plans['selection']['runtime_view_steps'] == 1 + assert plans['selection']['steps'][0] == { + 'source_step': 0, 'source': {'offset': 8, 'length': 0, 'to_end': True}, + 'offset': {'value': 0, 'from_u32le': True}, 'length': {'value': 4, 'from_u32le': True}} + document = '처음🙂 Pure는 필드를 연결한다.\n끝' + document_file = directory / 'document.txt' + document_file.write_bytes(document.encode('utf-8')) + with serving(binary, storage, program=program) as selected: + command = [sys.executable, str(ROOT / 'examples/select-text.py'), str(document_file), + '--key', 'text_selection', '--start', '2', '--end', '12', '--url', f'http://127.0.0.1:{selected.port}'] + result = subprocess.run(command, capture_output=True, text=True, timeout=15) + assert result.returncode == 0, (result.stdout, result.stderr) + expected = document[2:12].encode('utf-8') + assert json.loads(result.stdout) == {'revision': '1', 'selected': document[2:12], 'stored_bytes': len(expected)} + assert rpc(selected, 'GET', 'text_selection')[2] == expected + checks.append('text client uses character selection; named numeric fields select and persist the matching UTF-8 bytes') + + # Validation precedes the write: even a stale header cannot expose or + # commit partial input, and prior content/revision survive all failures. + invalid = [b'\0' * n for n in range(8)] + [ + struct.pack('<II', 0xffffffff, 0) + b'abc', + struct.pack('<II', 0, 0xffffffff) + b'abc', + struct.pack('<II', 2, 2) + b'abc', + struct.pack('<II', 4, 0) + b'abc'] + for payload in invalid: + assert rpc(selected, 'PUT', 'text_selection', payload, 1)[0] == 413 + status, headers, body = rpc(selected, 'GET', 'text_selection') + assert status == 200 and headers['ETag'] == '"1"' and body == expected + for key, payload in [('selection_empty', struct.pack('<II', 0, 0)), + ('selection_end', struct.pack('<II', 3, 0) + b'abc')]: + assert rpc(selected, 'PUT', key, payload, 0)[0] == 200 + assert rpc(selected, 'GET', key)[2] == b'' + assert rpc(selected, 'PUT', 'text_selection', struct.pack('<II', 0, 3) + b'abc', 0)[0] == 412 + checks.append('truncated layouts and invalid data-dependent ranges never change storage; empty and end selections remain valid') + records.append(selected.stats) + + declaration = 'layout E { tag: bytes[3]; count: u32le; start: u32le; document: bytes; } ' + get = 'on get { let o = store.read(request.key); reply(o, o.body); } ' + put = 'on put { let s = store.write(request.key, request.revision, request.body); reply(s); } ' + reordered = directory / 'reordered.pure' + reordered.write_text(declaration + get + 'on put { let e = view(E, request.body); let alias = e; ' + 'let part = range(alias.document, alias.start, alias.count); let again = part; ' + 'let s = store.write(request.key, request.revision, again); reply(s, part); }') + plans['reordered'] = json.loads(subprocess.check_output([str(binary), '--check-program', str(reordered)], text=True)) + assert plans['reordered']['runtime_view_steps'] == 1 + with serving(binary, storage, program=reordered) as moved: + status, headers, body = rpc(moved, 'PUT', 'reordered', b'tag' + struct.pack('<II', 4, 2) + b'abcdefgh', 0) + assert status == 200 and body == b'cdef' and headers['ETag'] == '"1"' + assert rpc(moved, 'GET', 'reordered')[2] == b'cdef' + records.append(moved.stats) + checks.append('changed declaration order and unaligned numeric fields determine access; aliases add no dynamic steps') + + unused = directory / 'unused-dynamic.pure' + unused.write_text(declaration + get + 'on put { let e = view(E, request.body); ' + 'let unused = range(e.document, e.start, e.count); ' + 'let s = store.write(request.key, request.revision, e.document); reply(s); }') + with serving(binary, storage, program=unused) as guarded: + assert rpc(guarded, 'PUT', 'unused_dynamic', b'tag' + struct.pack('<II', 4, 2) + b'abc', 0)[0] == 413 + assert rpc(guarded, 'GET', 'unused_dynamic')[0] == 404 + assert rpc(guarded, 'PUT', 'unused_dynamic', b'tag' + struct.pack('<II', 1, 1) + b'abc', 0)[0] == 200 + assert rpc(guarded, 'GET', 'unused_dynamic')[2] == b'abc' + records.append(guarded.stats) + checks.append('unused dynamic field selection retains its validation before the write effect') + + field_get = directory / 'field-get.pure' + field_get.write_text(declaration + 'on get { let o = store.read(request.key); ' + 'let e = view(E, o.body); reply(o, e.document); } ' + put) + plans['field_get'] = json.loads(subprocess.check_output([str(binary), '--check-program', str(field_get)], text=True)) + tail = '다른 길이의 문서'.encode('utf-8') + with serving(binary, storage, program=field_get) as writer: + assert rpc(writer, 'PUT', 'tail', b'tag' + struct.pack('<II', 0, 0) + tail, 0)[0] == 200 + assert rpc(writer, 'PUT', 'bad_layout', b'short', 0)[0] == 200 + assert rpc(writer, 'PUT', 'empty_tail', b'tag' + struct.pack('<II', 0, 0), 0)[0] == 200 + records.append(writer.stats) + with serving(binary, storage, budget=len(tail), program=field_get) as reader: + assert rpc(reader, 'GET', 'tail')[2] == tail + assert rpc(reader, 'GET', 'empty_tail')[2] == b'' + assert rpc(reader, 'GET', 'bad_layout')[0] == 413 + assert reader.stats['peak_retained_payload_bytes'] == len(tail) + records.append(reader.stats) + checks.append('named variable tail reads use the same file version and charge only the selected suffix') + + document_bytes = bytes(range(256)) * 4096 + payload = struct.pack('<II', 16, len(document_bytes) - 32) + document_bytes + with serving(binary, storage, budget=len(payload), program=program) as slow: + with socket.create_connection(('127.0.0.1', slow.port), timeout=8) as sock: + sock.sendall(header('PUT', 'dynamic_slow', len(payload), b'If-None-Match: *\r\n') + payload) + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == 200 and response.read(1) == document_bytes[16:17] + assert rpc(slow, 'GET', 'binary')[0] == 503 + assert response.read() == document_bytes[17:-16] + response.close() + assert rpc(slow, 'GET', 'binary')[2] == b'new' + assert rpc(slow, 'GET', 'dynamic_slow')[2] == document_bytes[16:-16] + assert slow.stats['peak_retained_payload_bytes'] == len(payload) + records.append(slow.stats) + checks.append('data-dependent response keeps and charges the original input through partial transmission') + return plans + + +def rpc_bytes(path): + # CLI export is pretty JSON; wire export is compact JSON with the same model. + return json.dumps(json.loads(path.read_text()), ensure_ascii=False, separators=(',', ':')).encode() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--server', type=Path, default=ROOT / 'build/pure-connections') + parser.add_argument('--output', type=Path) + parser.add_argument('--runtime-test', type=Path) + parser.add_argument('--splice-test', type=Path) + parser.add_argument('--editor', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + binary = args.server.resolve() + with tempfile.TemporaryDirectory(prefix='pure-connections-check-') as temporary: + result = check(binary, Path(temporary), args.editor.resolve() if args.editor else None) + if args.runtime_test: + runtime_test = args.runtime_test.resolve() + tested = subprocess.run([str(runtime_test)], cwd=ROOT, capture_output=True, text=True, timeout=30) + assert tested.returncode == 0, (tested.stdout, tested.stderr) + result['execution_core'] = json.loads(tested.stdout) + result['execution_core_test_sha256'] = hashlib.sha256(runtime_test.read_bytes()).hexdigest() + if args.splice_test: + splice_test = args.splice_test.resolve() + tested = subprocess.run([str(splice_test)], cwd=ROOT, capture_output=True, text=True, timeout=30) + assert tested.returncode == 0, (tested.stdout, tested.stderr) + result['document_edit_units'] = json.loads(tested.stdout) + result['document_edit_test_sha256'] = hashlib.sha256(splice_test.read_bytes()).hexdigest() + if args.editor: + result['editor_sha256'] = hashlib.sha256(args.editor.resolve().read_bytes()).hexdigest() + result['binary_sha256'] = hashlib.sha256(binary.read_bytes()).hexdigest() + result['environment'] = {'platform': platform.platform(), 'machine': platform.machine(), + 'python': platform.python_version(), 'node': subprocess.check_output(['node', '--version'], text=True).strip()} + result['source_sha256'] = {str(path.relative_to(ROOT.parent.parent)): hashlib.sha256(path.read_bytes()).hexdigest() + for path in [*(ROOT / name for name in ('flow.c', 'flow.h', 'store.c', 'store.h', 'program.c', 'program.h', + 'runtime.c', 'runtime.h', 'result.h', 'evaluate.c', 'store-port.c', 'store-port.h', 'test-runtime.c', + 'server.c', 'check.py', 'check_edits.py', 'edit.c', 'test-splice.c', + 'test-flow.c', 'test-program.c', 'test-fields.c', 'Makefile', + 'core-layout.c', 'check_core.py', 'core-budget.json', 'examples/editor.pure', + 'examples/storage.pure', 'examples/excerpt.pure', 'examples/selection.pure', 'examples/select-text.py')), + *(WORKBENCH / name for name in ('remote.mjs', 'cli.mjs', 'web/core.wasm', 'web/runtime.mjs', + 'web/compiler.mjs', 'web/project.mjs', 'examples/direct.pure'))]} + output = json.dumps(result, ensure_ascii=False, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open('x') as destination: + destination.write(output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_core.py b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_core.py new file mode 100644 index 0000000..321896e --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_core.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Fail when the selected execution core grows past its reviewed cost boundary. + +This is a release-build structural check using GNU-compatible ar/nm/size. Run it +separately from correctness/sanitizer tests; instrumented objects have other costs. +""" +import argparse +import hashlib +import json +from pathlib import Path +import platform +import subprocess + +ROOT = Path(__file__).resolve().parent + + +def run(*args): + return subprocess.check_output([str(arg) for arg in args], text=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--archive', type=Path, required=True) + parser.add_argument('--layout', type=Path, required=True) + parser.add_argument('--budget', type=Path, default=ROOT / 'core-budget.json') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + budget = json.loads(args.budget.read_text()) + archive = args.archive.resolve() + members = run('ar', 't', archive).splitlines() + layout = json.loads(run(args.layout.resolve())) + size_output = run('size', archive) + rows = [line.split() for line in size_output.splitlines() if line.split() and line.split()[0].isdigit()] + if len(rows) != len(members): + parser.error('unsupported size output; no footprint claim was made') + sizes = {name: sum(int(row[i]) for row in rows) for i, name in enumerate(('text', 'data', 'bss'))} + defined = {line.split()[-1] for line in run('nm', '-g', '--defined-only', archive).splitlines() + if len(line.split()) >= 3} + undefined = {line.split()[-1] for line in run('nm', '-u', archive).splitlines() + if len(line.split()) == 2 and line.split()[0] == 'U'} + external = sorted(undefined - defined) + failures = [] + if sorted(members) != sorted(budget['archive_members']): + failures.append('unexpected archive members') + for name, value in sizes.items(): + if value > budget[f'max_{name}_bytes']: + failures.append(f'{name}: {value} > {budget[f"max_{name}_bytes"]}') + for name, maximum in budget['layout_max_bytes'].items(): + if layout[name] > maximum: + failures.append(f'{name}: {layout[name]} > {maximum}') + extra = set(external) - set(budget['external_symbols_allowed']) + if extra: + failures.append(f'unexpected dependencies: {sorted(extra)}') + result = {'passed': not failures, 'failures': failures, 'budget': budget, + 'layout': layout, 'sections': sizes, 'members': members, + 'external_symbols': external, 'size_output': size_output, + 'archive_sha256': hashlib.sha256(archive.read_bytes()).hexdigest(), + 'layout_binary_sha256': hashlib.sha256(args.layout.resolve().read_bytes()).hexdigest(), + 'budget_sha256': hashlib.sha256(args.budget.read_bytes()).hexdigest(), + 'platform': platform.platform(), 'size_tool': run('size', '--version').splitlines()[0]} + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open('x') as destination: + destination.write(output) + print(output, end='') + if failures: + raise SystemExit(1) + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_edits.py b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_edits.py new file mode 100644 index 0000000..1a6dc53 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/check_edits.py @@ -0,0 +1,108 @@ +"""Real native automation and HTTP clients editing the same authored document.""" +from concurrent.futures import ThreadPoolExecutor +import json +import socket +import struct +import subprocess + + +def check_edits(editor, binary, root, directory, serving, rpc, header, records, checks): + storage = directory / 'edited-objects' + program = root / 'examples/editor.pure' + file = directory / 'insertion.bin' + + def local(*args, body=None, code=0, source=program, budget=None): + if body is not None: + file.write_bytes(body) + args = (*args, str(file)) + command = [str(editor), '--program', str(source), '--store', str(storage)] + if budget is not None: + command += ['--budget', str(budget)] + result = subprocess.run(command + [str(arg) for arg in args], capture_output=True, timeout=20) + assert result.returncode == code, (command, args, result.stdout, result.stderr) + return result + + initial = 'Pure에서 안녕 🌏\n'.encode() + created = local('edit', 'note', 0, 0, 0, body=initial) + assert json.loads(created.stdout)['revision'] == '1' + start = initial.index('안녕'.encode()) + inserted = '반가워'.encode() + expected = initial[:start] + inserted + initial[start + len('안녕'.encode()):] + local('edit', 'note', 1, start, len('안녕'.encode()), body=inserted) + shown = local('show', 'note') + assert shown.stdout == expected and json.loads(shown.stderr)['revision'] == '2' + checks.append('native authored byte edit and fresh native process restore UTF-8 content') + stale = local('edit', 'note', 1, 0, 0, body=b'bad', code=3) + assert json.loads(stale.stderr) == {'error': 'conflict', 'revision': '2'} + local('edit', 'note', 2, 0xffffffff, 0, body=b'bad', code=2) + assert local('show', 'note').stdout == expected + checks.append('native stale revision and invalid byte position preserve the committed document') + + reordered = directory / 'edit-reordered.pure' + reordered.write_text('layout E { tag: bytes[1]; remove: u32le; start: u32le; body: bytes; } ' + 'on get { let o = store.read(request.key); reply(o, o.body); } ' + 'on put { let e = view(E, request.body); ' + 'let s = store.splice(request.key, request.revision, e.start, e.remove, e.body); reply(s, e.body); }') + local('edit', 'note', 2, len(expected), 0, body=b'!', source=reordered) + expected += b'!' + assert local('show', 'note', source=reordered).stdout == expected + checks.append('native convenience editor derives unaligned reordered field positions from the authored program') + + custom = directory / 'edit-custom.pure' + custom.write_text('on get { let o = store.read(request.key); reply(o, o.body); } ' + 'on put { let s = store.splice(request.key, request.revision, 0, 0, request.body); reply(s); }') + local('edit', 'custom', 0, 0, 0, body=b'ignored', source=custom, code=2) + applied = local('apply', 'custom', 0, body=b'custom input', source=custom) + assert json.loads(applied.stdout) == {'revision': '1', 'reply_bytes': 0} + assert local('show', 'custom', source=custom).stdout == b'custom input' + checks.append('raw native apply runs another authored input contract without changing the executor') + + with serving(binary, storage, program=program) as server: + assert rpc(server, 'GET', 'note')[2] == expected + status, headers, body = rpc(server, 'PUT', 'note', struct.pack('<II', len(expected), 0) + b'?', 3) + assert status == 200 and headers['ETag'] == '"4"' and body == b'?' + expected += b'?' + assert local('show', 'note').stdout == expected + local('edit', 'note', 4, 0, 4, body=b'PURE') + expected = b'PURE' + expected[4:] + assert rpc(server, 'PUT', 'note', struct.pack('<II', 0, 0) + b'stale', 4)[0] == 412 + assert rpc(server, 'GET', 'note')[2] == expected + checks.append('native CLI and external HTTP share revisions and the same authored storage effect in both directions') + with socket.create_connection(('127.0.0.1', server.port), timeout=8) as sock: + sock.sendall(header('PUT', 'note', 12, b'If-Match: "5"\r\n') + b'partial') + sock.shutdown(socket.SHUT_WR) + assert sock.recv(1) == b'' + assert rpc(server, 'PUT', 'note', b'short', 5)[0] == 413 + assert rpc(server, 'PUT', 'note', struct.pack('<II', 0xffffffff, 0), 5)[0] == 413 + assert rpc(server, 'GET', 'note')[2] == expected + checks.append('partial or invalid edit input never modifies the stored document') + with serving(binary, storage, program=program) as other: + with ThreadPoolExecutor(max_workers=2) as pool: + tasks = [pool.submit(rpc, target, 'PUT', 'note', struct.pack('<II', 0, 0) + value, 5) + for target, value in ((server, b'A'), (other, b'B'))] + statuses = [task.result()[0] for task in tasks] + assert statuses.count(200) == 1 and all(status in (200, 412, 503) for status in statuses) + status, headers, body = rpc(server, 'GET', 'note') + assert status == 200 and headers['ETag'] == '"6"' + assert body == (b'A' if statuses[0] == 200 else b'B') + expected + records.append(other.stats) + checks.append('two processes cannot both splice the same expected document revision') + records.append(server.stats) + + # This tests retained input size for a real large edit, not throughput. + large = bytes(range(256)) * 4096 + local('edit', 'large', 0, 0, 0, body=large) + local('edit', 'large', 1, 524287, 3, body=b'XYZ', budget=11) + expected_large = large[:524287] + b'XYZ' + large[524290:] + assert local('show', 'large').stdout == expected_large + with serving(binary, storage, program=program, budget=11) as bounded: + status, headers, body = rpc(bounded, 'PUT', 'large', struct.pack('<II', 524287, 3) + b'abc', 2) + assert status == 200 and headers['ETag'] == '"3"' and body == b'abc' + assert rpc(bounded, 'PUT', 'large', struct.pack('<II', 0, 0) + b'abcd', 3)[0] == 503 + assert bounded.stats['peak_retained_payload_bytes'] == 11 + records.append(bounded.stats) + assert local('show', 'large').stdout == large[:524287] + b'abc' + large[524290:] + checks.append('1 MiB document changes with 11 input credits; larger input is rejected before an effect') + return {'large_document_bytes': len(large), 'small_edit_input_bytes': 11, + 'storage_rewrites_full_extent': True, 'storage_scratch_bytes': 4096, + 'kind': 'correctness-and-admission-not-total-memory-or-performance'} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-budget.json b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-budget.json new file mode 100644 index 0000000..82d830b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-budget.json @@ -0,0 +1,15 @@ +{ + "profile": "current-storage-execution-core", + "max_text_bytes": 6144, + "max_data_bytes": 0, + "max_bss_bytes": 0, + "layout_max_bytes": { + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680 + }, + "archive_members": ["runtime.o", "evaluate.o"], + "external_symbols_allowed": ["memcpy", "memmove", "memset", "strlen"], + "scope": "Release object sections and C bookkeeping, validated with GNU tools on Linux AArch64. Not total RAM, process RSS, stack, linked libraries, parser, storage, network or UI. 6 KiB is a regression ceiling for this profile, not a universal runtime size promise." +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-layout.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-layout.c new file mode 100644 index 0000000..6687be7 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/core-layout.c @@ -0,0 +1,11 @@ +#include "runtime.h" +#include <stdio.h> + +/* Host-side measurement tool; never linked into libpure-execution.a. */ +int main(void) { + printf("{\"domain_bytes\":%zu,\"work_slot_bytes\":%zu,\"ticket_bytes\":%zu," + "\"prepared_program_bytes\":%zu,\"pointer_bytes\":%zu}\n", + sizeof(PureRuntime), sizeof(PureWork), sizeof(PureTicket), + sizeof(PureProgram), sizeof(void *)); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/edit.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/edit.c new file mode 100644 index 0000000..4daad14 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/edit.c @@ -0,0 +1,153 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include <inttypes.h> +#include <stdio.h> +#include <string.h> + +/* Native automation entry for the same prepared document program as the + * network adapter. No socket or body serialization in the local execution. */ +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 void put32(unsigned char *to, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) to[i] = (unsigned char)(value >> (8 * i)); +} + +/* The convenience command fills the program's two numeric splice arguments + * and its trailing insertion bytes. A more general input uses apply. */ +static bool edit_layout(const PureProgram *p, size_t *prefix) { + if (!p->splice || !p->write.to_end || p->write_step || p->input_steps || + !p->splice_offset.from_u32le || !p->splice_remove.from_u32le) return false; + size_t start = p->splice_offset.value, remove = p->splice_remove.value; + *prefix = p->write.offset; + if (*prefix > PURE_OBJECT_LIMIT || p->input_required > *prefix || start > *prefix || + remove > *prefix || *prefix - start < 4 || *prefix - remove < 4) return false; + return start < remove ? remove - start >= 4 : start - remove >= 4; +} + +int main(int argc, char **argv) { + const char *program_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], "--program")) program_path = argv[at + 1]; + else if (!strcmp(argv[at], "--store")) store_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 (!program_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; + PureProgram program; + char error[160]; + if (!pure_program_load(program_path, &program, error, sizeof(error))) { + fprintf(stderr, "Program: %s\n", error); return 2; + } + size_t prefix = 0; + if (edit && !edit_layout(&program, &prefix)) { + fputs("edit requires two distinct u32le splice fields 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; + } + PureRuntime runtime; + PureWork work[1]; + if (!pure_runtime_init(&runtime, work, 1, &program, pure_store_host(store_path), budget)) { + if (file) fclose(file); + return 2; + } + PureTicket ticket; + PureResult admitted = pure_runtime_accept(&runtime, show ? PURE_INPUT_READ : PURE_INPUT_WRITE, + argv[at + 1], (int64_t)expected, prefix + length, &ticket); + if (admitted != PURE_OK) { + if (file) fclose(file); + fprintf(stderr, "%s\n", message(admitted)); return 2; + } + if (!show) { + unsigned char *input = pure_runtime_ingress(ticket, NULL); + if (prefix) memset(input, 0, prefix); + if (edit) { + put32(input + program.splice_offset.value, (uint32_t)offset); + put32(input + program.splice_remove.value, (uint32_t)remove); + } + bool valid = !length || fread(input + prefix, 1, length, file) == length; + if (fgetc(file) != EOF || ferror(file)) valid = false; + if (fclose(file)) valid = false; + if (!valid) { + pure_runtime_release(ticket); + fputs("input changed or could not be read; nothing was applied\n", stderr); return 2; + } + if (!pure_runtime_ready(ticket)) { pure_runtime_release(ticket); return 2; } + } + PureOutcome outcome; + while (!pure_runtime_result(ticket, &outcome)) { + if (!pure_runtime_step(&runtime)) { pure_runtime_release(ticket); return 2; } + } + int status = 0; + if (outcome.status != PURE_OK) { + fprintf(stderr, "{\"error\":\"%s\",\"revision\":\"%" PRId64 "\"}\n", message(outcome.status), outcome.revision); + status = outcome.status == PURE_CONFLICT ? 3 : 2; + } else if (show) { + if (outcome.body.size && fwrite(outcome.body.data, 1, outcome.body.size, stdout) != outcome.body.size) status = 2; + fprintf(stderr, "{\"revision\":\"%" PRId64 "\",\"bytes\":%zu}\n", outcome.revision, outcome.body.size); + } else { + if (printf("{\"revision\":\"%" PRId64 "\",\"reply_bytes\":%zu}\n", outcome.revision, outcome.body.size) < 0) status = 2; + } + if (fflush(stdout)) status = 2; + pure_runtime_release(ticket); + return status; +usage: + fputs("pure-edit --program FILE --store DIRECTORY [--budget BYTES] show KEY\n" + "pure-edit --program FILE --store DIRECTORY [--budget BYTES] edit KEY REVISION BYTE_OFFSET REMOVE_BYTES INSERT_FILE\n" + "pure-edit --program FILE --store DIRECTORY [--budget BYTES] apply KEY REVISION INPUT_FILE\n" + "edit uses the prepared program's field locations; apply accepts its raw input bytes.\n", stderr); + return 2; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/evaluate.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/evaluate.c new file mode 100644 index 0000000..88ffae7 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/evaluate.c @@ -0,0 +1,53 @@ +#include "program.h" +#include <stdint.h> + +bool pure_region_span(PureRegion region, PureSpan source, PureSpan *out) { + *out = (PureSpan){0}; + if (region.offset > source.size) return false; + size_t length = region.to_end ? source.size - region.offset : region.length; + if (length > source.size - region.offset) return false; + *out = (PureSpan){length ? source.data + region.offset : NULL, length}; + return true; +} + +static bool index_read(PureIndex index, PureSpan source, size_t *out) { + if (!index.from_u32le) { *out = index.value; return true; } + if (index.value > source.size || 4 > source.size - index.value) return false; + const unsigned char *bytes = source.data + index.value; + *out = (size_t)bytes[0] | (size_t)bytes[1] << 8 | (size_t)bytes[2] << 16 | (size_t)bytes[3] << 24; + return true; +} + +bool pure_program_splice(const PureProgram *p, PureSpan source, size_t *offset, size_t *remove) { + return p->splice && index_read(p->splice_offset, source, offset) && + index_read(p->splice_remove, source, remove); +} + +static bool resolve(PureRegion region, size_t step, PureSpan source, + const PureSpan *values, size_t count, PureSpan *out) { + if (!step) return pure_region_span(region, source, out); + if (step > count) return false; + *out = values[step - 1]; + return true; +} + +bool pure_program_input(const PureProgram *p, PureSpan source, PureSpan *write, PureSpan *reply) { + *write = (PureSpan){0}; + *reply = (PureSpan){0}; + if (source.size < p->input_required || p->input_steps > PURE_DYNAMIC_LIMIT) return false; + PureSpan values[PURE_DYNAMIC_LIMIT]; + for (size_t i = 0; i < p->input_steps; ++i) { + const PureRangeStep *step = &p->steps[i]; + PureSpan parent; + size_t offset, length; + if (!resolve(step->source, step->source_step, source, values, i, &parent) || + !index_read(step->offset, source, &offset) || !index_read(step->length, source, &length) || + !pure_region_span((PureRegion){.offset = offset, .length = length}, parent, &values[i])) return false; + } + PureSpan stored, returned = {0}; + if (!resolve(p->write, p->write_step, source, values, p->input_steps, &stored) || + (p->reply_input && !resolve(p->reply, p->reply_step, source, values, p->input_steps, &returned))) return false; + *write = stored; + *reply = returned; + return true; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/editor.pure b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/editor.pure new file mode 100644 index 0000000..4de78bc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/editor.pure @@ -0,0 +1,19 @@ +// A revision-checked byte edit. The input carries only the replacement. +// UTF-8 cursor/grapheme positions are translated to byte ranges by the UI. +layout Edit { + start: u32le; + remove: u32le; + insert: bytes; +} + +on get { + let document = store.read(request.key); + reply(document, document.body); +} + +on put { + let edit = view(Edit, request.body); + let saved = store.splice(request.key, request.revision, + edit.start, edit.remove, edit.insert); + reply(saved, edit.insert); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/excerpt.pure b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/excerpt.pure new file mode 100644 index 0000000..622f9bb --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/excerpt.pure @@ -0,0 +1,13 @@ +// Read bytes 32..47 of an object that must contain the complete 64-byte window. +// Preparation keeps the size check for 16..79, and maps only 32..47 for reply. +on get { + let object = store.read(request.key); + let window = range(object.body, 16, 64); + let excerpt = range(window, 16, 16); + reply(object, excerpt); +} + +on put { + let saved = store.write(request.key, request.revision, request.body); + reply(saved); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/select-text.py b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/select-text.py new file mode 100644 index 0000000..ffaf80b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/select-text.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Save a UTF-8 document selection through selection.pure (character indices).""" +import argparse +import http.client +import json +from pathlib import Path +import re +import struct +from urllib.parse import urlsplit + +LIMIT = (2 * 6 + 32) * 32768 + 8192 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('file', type=Path) + parser.add_argument('--key', required=True) + parser.add_argument('--start', type=int, required=True, help='first Unicode code point, starting at 0') + parser.add_argument('--end', type=int, required=True, help='exclusive Unicode code point boundary') + parser.add_argument('--revision', type=int, default=0, help='0 creates; otherwise expected stored revision') + parser.add_argument('--url', default='http://127.0.0.1:8767') + args = parser.parse_args() + url = urlsplit(args.url) + try: + port = url.port or 80 + except ValueError as error: + parser.error(str(error)) + if (url.scheme != 'http' or url.hostname not in ['localhost', '127.0.0.1'] or + url.username is not None or url.password is not None or url.path not in ['', '/'] or + url.query or url.fragment): + parser.error('use a local HTTP server origin') + if not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', args.key) or not 0 <= args.revision <= 2**63 - 1: + parser.error('invalid object key or revision') + try: + with args.file.open('rb') as source: + document = source.read(LIMIT - 8 + 1) + if len(document) > LIMIT - 8: + parser.error('document exceeds the prototype request limit') + text = document.decode('utf-8') + except (OSError, UnicodeError) as error: + parser.error(str(error)) + if not 0 <= args.start <= args.end <= len(text): + parser.error('selection is outside the document') + start = len(text[:args.start].encode('utf-8')) + selected = text[args.start:args.end].encode('utf-8') + # The boundary layout is explicit; storage receives only selected bytes. + payload = struct.pack('<II', start, len(selected)) + document + headers = {'Content-Type': 'application/octet-stream'} + headers['If-None-Match' if args.revision == 0 else 'If-Match'] = ( + '*' if args.revision == 0 else f'"{args.revision}"') + connection = http.client.HTTPConnection(url.hostname, port, timeout=12) + try: + connection.request('PUT', '/objects/' + args.key, payload, headers) + response = connection.getresponse() + body = response.read(LIMIT + 1) + if response.status != 200: + parser.error(f'storage response {response.status}: {response.getheader("X-Pure-Error")}') + if body != selected: + parser.error('response differs from the selection; check the installed program (write may have occurred)') + revision = response.getheader('ETag', '') + if not re.fullmatch(r'"[1-9][0-9]*"', revision) or int(revision[1:-1]) > 2**63 - 1: + parser.error('response has no valid stored revision (write may have occurred)') + except (OSError, http.client.HTTPException) as error: + parser.error(f'{error}; storage outcome is unknown if the request was sent') + finally: + connection.close() + print(json.dumps({'revision': revision[1:-1], 'selected': selected.decode('utf-8'), + 'stored_bytes': len(selected)}, ensure_ascii=False)) + + +if __name__ == '__main__': + main() diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/selection.pure b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/selection.pure new file mode 100644 index 0000000..7d9d4ec --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/selection.pure @@ -0,0 +1,19 @@ +// This is an input boundary layout, not a required storage or RAM model. +// The client sends two little-endian u32 values followed by document bytes. +layout Selection { + start: u32le; + count: u32le; + document: bytes; +} + +on get { + let saved = store.read(request.key); + reply(saved, saved.body); +} + +on put { + let input = view(Selection, request.body); + let selected = range(input.document, input.start, input.count); + let saved = store.write(request.key, request.revision, selected); + reply(saved, selected); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/storage.pure b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/storage.pure new file mode 100644 index 0000000..ff54ea3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/examples/storage.pure @@ -0,0 +1,11 @@ +// The host supplies request bytes, an object name, and the expected revision. +// Failed storage results propagate to the host's response without executing reply. +on get { + let object = store.read(request.key); + reply(object, object.body); +} + +on put { + let saved = store.write(request.key, request.revision, request.body); + reply(saved); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.c new file mode 100644 index 0000000..1070c70 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.c @@ -0,0 +1,41 @@ +#include "flow.h" +#include <assert.h> + +void pure_transfer_abort(PureTransfer *t) { + if (!t->active) return; + PureRelease release = t->release; + void *owner = t->owner; + *t = (PureTransfer){0}; + if (release) release(owner); +} + +static void advance(PureTransfer *t) { + while (t->active && t->part < 2 && t->offset == t->spans[t->part].size) { + t->part++; + t->offset = 0; + } + if (t->active && t->part == 2) pure_transfer_abort(t); +} + +void pure_transfer_init(PureTransfer *t, PureSpan header, PureSpan body, + void *owner, PureRelease release) { + assert(!t->active); + assert((!header.size || header.data) && (!body.size || body.data)); + *t = (PureTransfer){.spans = {header, body}, .owner = owner, + .release = release, .active = true}; + advance(t); +} + +PureSpan pure_transfer_next(const PureTransfer *t, size_t limit) { + if (!t->active) return (PureSpan){0}; + PureSpan span = t->spans[t->part]; + size_t remaining = span.size - t->offset; + return (PureSpan){span.data + t->offset, remaining < limit ? remaining : limit}; +} + +bool pure_transfer_consume(PureTransfer *t, size_t bytes) { + if (!t->active || bytes > t->spans[t->part].size - t->offset) return false; + t->offset += bytes; + advance(t); + return true; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.h b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.h new file mode 100644 index 0000000..6e27c58 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/flow.h @@ -0,0 +1,25 @@ +#ifndef PURE_FLOW_H +#define PURE_FLOW_H +#include <stdbool.h> +#include <stddef.h> + +typedef struct { const unsigned char *data; size_t size; } PureSpan; +typedef void (*PureRelease)(void *owner); + +/* A borrowed body stays alive until every byte is consumed or the transfer is + * aborted. A successful send() consumes bytes; peer receipt is a separate fact. + * Single owner thread; callbacks must not reenter or reuse this transfer. */ +typedef struct { + PureSpan spans[2]; + size_t part, offset; + void *owner; + PureRelease release; + bool active; +} PureTransfer; + +void pure_transfer_init(PureTransfer *, PureSpan header, PureSpan body, + void *owner, PureRelease); +PureSpan pure_transfer_next(const PureTransfer *, size_t limit); +bool pure_transfer_consume(PureTransfer *, size_t bytes); +void pure_transfer_abort(PureTransfer *); +#endif diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.c new file mode 100644 index 0000000..8680782 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.c @@ -0,0 +1,366 @@ +#include "program.h" +#include "store.h" +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +enum { NAME = 256, NUMBER, SYMBOLS = 32, LAYOUTS = 4, FIELDS = 16 }; +typedef enum { BYTE_VALUE, RESULT_VALUE, LAYOUT_VALUE, INDEX_VALUE } ValueKind; +typedef struct { + char name[32]; + ValueKind kind; + PureRegion region; + size_t step, layout; + PureIndex index; +} Symbol; +typedef struct { + char name[32]; + size_t offset, length; + bool number, tail; +} Field; +typedef struct { + char name[32]; + Field fields[FIELDS]; + size_t count, prefix; + bool tail; +} Layout; +typedef struct { + const char *source; + size_t length, at, line, token_line; + int token; + char name[32]; + size_t number; + char *error; + size_t error_size; + bool failed, put, effect; + Symbol symbols[SYMBOLS]; + size_t count, layout_count; + Layout layouts[LAYOUTS]; + PureProgram program; +} Parser; + +static bool reject(Parser *p, const char *message) { + if (!p->failed && p->error_size) snprintf(p->error, p->error_size, "line %zu: %s", p->token_line, message); + p->failed = true; + return false; +} + +static bool letter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +static bool next(Parser *p) { + while (p->at < p->length) { + char c = p->source[p->at]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + if (c == '\n') ++p->line; + ++p->at; + } else if (c == '/' && p->at + 1 < p->length && p->source[p->at + 1] == '/') { + while (p->at < p->length && p->source[p->at] != '\n') ++p->at; + } else break; + } + p->token_line = p->line; + p->name[0] = 0; + p->token = 0; + if (p->at == p->length) return true; + char c = p->source[p->at++]; + if (letter(c)) { + size_t start = p->at - 1; + while (p->at < p->length && (letter(p->source[p->at]) || + (p->source[p->at] >= '0' && p->source[p->at] <= '9'))) ++p->at; + size_t size = p->at - start; + if (size >= sizeof(p->name)) return reject(p, "name exceeds 31 bytes"); + memcpy(p->name, p->source + start, size); + p->name[size] = 0; + p->token = NAME; + } else if (c >= '0' && c <= '9') { + size_t value = (size_t)(c - '0'); + while (p->at < p->length && p->source[p->at] >= '0' && p->source[p->at] <= '9') { + unsigned digit = (unsigned)(p->source[p->at++] - '0'); + if (value > (SIZE_MAX - digit) / 10) return reject(p, "integer overflow"); + value = value * 10 + digit; + } + p->number = value; + p->token = NUMBER; + } else if (c && strchr("{}().,=;:[]", c)) p->token = (unsigned char)c; + else return reject(p, "unexpected character"); + return true; +} + +static bool is_word(Parser *p, const char *word) { + return p->token == NAME && !strcmp(p->name, word); +} + +static bool word(Parser *p, const char *expected) { + return is_word(p, expected) ? next(p) : reject(p, expected); +} + +static bool token(Parser *p, int expected) { + return p->token == expected ? next(p) : reject(p, "unexpected token"); +} + +static Symbol *find(Parser *p, const char *name) { + for (size_t i = 0; i < p->count; ++i) + if (!strcmp(name, p->symbols[i].name)) return &p->symbols[i]; + return NULL; +} + +static bool field(Parser *p, const char *object, const char *name) { + return word(p, object) && token(p, '.') && word(p, name); +} + +static bool require_bytes(Parser *p, const Symbol *value) { + return value->kind == BYTE_VALUE || reject(p, "expected bytes; select a byte field"); +} + +static void require_length(Parser *p, size_t required) { + size_t *minimum = p->put ? &p->program.input_required : &p->program.read_required; + if (required > *minimum) *minimum = required; +} + +static Layout *find_layout(Parser *p, const char *name) { + for (size_t i = 0; i < p->layout_count; ++i) + if (!strcmp(name, p->layouts[i].name)) return &p->layouts[i]; + return NULL; +} + +static bool layout_definition(Parser *p) { + if (p->layout_count == LAYOUTS) return reject(p, "program exceeds 4 layouts"); + if (!next(p)) return false; + if (p->token != NAME || find_layout(p, p->name)) return reject(p, "expected a unique layout name"); + Layout *layout = &p->layouts[p->layout_count]; + strcpy(layout->name, p->name); + if (!next(p) || !token(p, '{')) return false; + while (p->token != '}') { + if (layout->count == FIELDS) return reject(p, "layout exceeds 16 fields"); + if (layout->tail) return reject(p, "variable bytes must be the last field"); + if (p->token != NAME) return reject(p, "expected a field name"); + for (size_t i = 0; i < layout->count; ++i) + if (!strcmp(layout->fields[i].name, p->name)) return reject(p, "duplicate field"); + Field *f = &layout->fields[layout->count]; + strcpy(f->name, p->name); + f->offset = layout->prefix; + if (!next(p) || !token(p, ':')) return false; + if (is_word(p, "u32le")) { + f->number = true; + f->length = 4; + if (!next(p)) return false; + } else if (is_word(p, "bytes")) { + if (!next(p)) return false; + if (p->token == '[') { + if (!next(p)) return false; + if (p->token != NUMBER) return reject(p, "fixed bytes requires a constant size"); + f->length = p->number; + if (!next(p) || !token(p, ']')) return false; + } else { f->tail = true; layout->tail = true; } + } else return reject(p, "supported field types: u32le, bytes[N], bytes"); + if (f->length > PURE_OBJECT_LIMIT - layout->prefix) return reject(p, "layout exceeds object bound"); + layout->prefix += f->length; + ++layout->count; + if (!token(p, ';')) return false; + } + if (!layout->count) return reject(p, "empty layout"); + ++p->layout_count; + return token(p, '}'); +} + +static bool reference(Parser *p, Symbol *out) { + if (is_word(p, "request")) { + if (!p->put) return reject(p, "request.body is only available in on put"); + *out = (Symbol){.kind = BYTE_VALUE, .region = {.to_end = true}}; + return field(p, "request", "body"); + } + Symbol *symbol = p->token == NAME ? find(p, p->name) : NULL; + if (!symbol) return reject(p, "unknown value"); + *out = *symbol; + if (!next(p)) return false; + if (p->token != '.') return true; + if (!next(p)) return false; + if (symbol->kind == RESULT_VALUE) { + if (p->put) return reject(p, "write result has no body; use the input value"); + *out = (Symbol){.kind = BYTE_VALUE, .region = {.to_end = true}}; + return word(p, "body"); + } + if (symbol->kind != LAYOUT_VALUE || p->token != NAME) return reject(p, "expected a layout field"); + Layout *layout = &p->layouts[symbol->layout]; + Field *f = NULL; + for (size_t i = 0; i < layout->count; ++i) + if (!strcmp(p->name, layout->fields[i].name)) f = &layout->fields[i]; + if (!f) return reject(p, "unknown layout field"); + size_t offset = symbol->region.offset + f->offset; + *out = (Symbol){.kind = f->number ? INDEX_VALUE : BYTE_VALUE}; + if (f->number) out->index = (PureIndex){.value = offset, .from_u32le = true}; + else out->region = (PureRegion){.offset = offset, + .length = f->tail ? (symbol->region.to_end ? 0 : symbol->region.length - f->offset) : f->length, + .to_end = f->tail && symbol->region.to_end}; + return next(p); +} + +static bool byte_reference(Parser *p, Symbol *out) { + return reference(p, out) && require_bytes(p, out); +} + +static bool index_value(Parser *p, PureIndex *out) { + if (p->token == NUMBER) { + *out = (PureIndex){.value = p->number}; + return next(p); + } + Symbol value; + if (!reference(p, &value)) return false; + if (value.kind != INDEX_VALUE) return reject(p, "range requires an integer or u32le field"); + *out = value.index; + return true; +} + +static bool layout_view(Parser *p, Symbol *out) { + if (!next(p) || !token(p, '(')) return false; + Layout *layout = p->token == NAME ? find_layout(p, p->name) : NULL; + if (!layout) return reject(p, "unknown layout"); + if (!next(p) || !token(p, ',')) return false; + Symbol source; + if (!byte_reference(p, &source) || !token(p, ')')) return false; + if (source.step) return reject(p, "layout view requires a statically located source"); + size_t bound = source.region.to_end ? PURE_OBJECT_LIMIT - source.region.offset : source.region.length; + if (layout->prefix > bound) return reject(p, "layout exceeds source bound"); + require_length(p, source.region.offset + layout->prefix); + *out = (Symbol){.kind = LAYOUT_VALUE, .region = source.region, .layout = (size_t)(layout - p->layouts)}; + if (!layout->tail) { out->region.length = layout->prefix; out->region.to_end = false; } + return true; +} + +static bool view(Parser *p, Symbol *out) { + if (is_word(p, "view")) return layout_view(p, out); + if (!is_word(p, "range")) return reference(p, out); + Symbol parent; + PureIndex offset, length; + if (!next(p) || !token(p, '(') || !byte_reference(p, &parent) || !token(p, ',') || + !index_value(p, &offset) || !token(p, ',') || !index_value(p, &length) || !token(p, ')')) return false; + bool dynamic = parent.step || offset.from_u32le || length.from_u32le; + *out = (Symbol){.kind = BYTE_VALUE}; + if (dynamic) { + if (!p->put) return reject(p, "data-dependent ranges currently require PUT input"); + if (p->program.input_steps == PURE_DYNAMIC_LIMIT) return reject(p, "input exceeds 8 dynamic ranges"); + if ((!offset.from_u32le && offset.value > PURE_OBJECT_LIMIT) || + (!length.from_u32le && length.value > PURE_OBJECT_LIMIT)) return reject(p, "range exceeds object bound"); + PureRangeStep *step = &p->program.steps[p->program.input_steps++]; + *step = (PureRangeStep){.source = parent.region, .source_step = parent.step, + .offset = offset, .length = length}; + out->step = p->program.input_steps; + } else { + size_t bound = parent.region.to_end ? PURE_OBJECT_LIMIT - parent.region.offset : parent.region.length; + if (offset.value > bound || length.value > bound - offset.value) return reject(p, "range exceeds its source bound"); + out->region = (PureRegion){.offset = parent.region.offset + offset.value, .length = length.value}; + require_length(p, out->region.offset + out->region.length); + } + ++p->program.range_definitions; + return true; +} + +static bool effect(Parser *p, Symbol *out) { + if (p->effect) return reject(p, "only one storage effect is supported per handler"); + if (!word(p, "store") || !token(p, '.')) return false; + bool splice = p->put && is_word(p, "splice"); + if (!word(p, p->put ? (splice ? "splice" : "write") : "read") || + !token(p, '(') || !field(p, "request", "key")) return false; + if (p->put) { + Symbol value; + if (!token(p, ',') || !field(p, "request", "revision") || !token(p, ',')) return false; + if (splice) { + if (!index_value(p, &p->program.splice_offset) || !token(p, ',') || + !index_value(p, &p->program.splice_remove) || !token(p, ',')) return false; + if ((!p->program.splice_offset.from_u32le && p->program.splice_offset.value > PURE_OBJECT_LIMIT) || + (!p->program.splice_remove.from_u32le && p->program.splice_remove.value > PURE_OBJECT_LIMIT)) + return reject(p, "splice exceeds object bound"); + p->program.splice = true; + } + if (!byte_reference(p, &value)) return false; + p->program.write = value.region; + p->program.write_step = value.step; + } + if (!token(p, ')')) return false; + out->kind = RESULT_VALUE; + p->effect = true; + return true; +} + +static bool handler(Parser *p) { + p->count = 0; + p->effect = false; + if (!token(p, '{')) return false; + while (is_word(p, "let")) { + if (p->put && p->effect) return reject(p, "all views must precede the storage effect; reply must follow it"); + if (p->count == SYMBOLS) return reject(p, "handler exceeds 32 definitions"); + if (!next(p)) return false; + if (p->token != NAME || is_word(p, "request") || is_word(p, "store") || + is_word(p, "range") || is_word(p, "view")) return reject(p, "expected a non-reserved definition name"); + if (find(p, p->name)) return reject(p, "duplicate definition"); + char name[32]; + strcpy(name, p->name); + Symbol symbol = {0}; + if (!next(p) || !token(p, '=')) return false; + if (is_word(p, "store")) { + if (!effect(p, &symbol)) return false; + } else { + if (!view(p, &symbol)) return false; + ++p->program.view_definitions; + } + if (!token(p, ';')) return false; + strcpy(symbol.name, name); + p->symbols[p->count++] = symbol; + } + if (!p->effect) return reject(p, "handler requires its storage effect"); + if (!word(p, "reply") || !token(p, '(')) return false; + Symbol *result = p->token == NAME ? find(p, p->name) : NULL; + if (!result || result->kind != RESULT_VALUE) return reject(p, "reply requires this handler's storage result"); + if (!next(p)) return false; + Symbol body = {0}; + bool has_body = p->token == ','; + if (has_body && (!next(p) || !byte_reference(p, &body))) return false; + if (!token(p, ')') || !token(p, ';') || !token(p, '}')) return false; + if (p->put) { + p->program.reply = body.region; + p->program.reply_step = body.step; + p->program.reply_input = has_body; + } else p->program.read = body.region; + return true; +} + +bool pure_program_parse(const char *source, size_t length, PureProgram *out, + char *error, size_t error_size) { + Parser p = {.source = source, .length = length, .line = 1, .token_line = 1, + .error = error, .error_size = error_size}; + if (error_size) error[0] = 0; + if (length > PURE_PROGRAM_LIMIT) return reject(&p, "program exceeds 16 KiB"); + if (!next(&p)) return false; + while (is_word(&p, "layout")) if (!layout_definition(&p)) return false; + unsigned handlers = 0; + while (p.token) { + if (!word(&p, "on")) return false; + if (!is_word(&p, "get") && !is_word(&p, "put")) return reject(&p, "expected get or put handler"); + p.put = is_word(&p, "put"); + unsigned bit = p.put ? 2 : 1; + if (handlers & bit) return reject(&p, "duplicate handler"); + handlers |= bit; + if (!next(&p) || !handler(&p)) return false; + } + if (handlers != 3) return reject(&p, "both get and put handlers are required"); + *out = p.program; /* Invalid preparation never changes the installed plan. */ + return true; +} + +bool pure_program_load(const char *path, PureProgram *out, char *error, size_t error_size) { + FILE *file = fopen(path, "rb"); + if (!file) { + if (error_size) snprintf(error, error_size, "cannot open program"); + return false; + } + char source[PURE_PROGRAM_LIMIT + 1]; + size_t length = fread(source, 1, sizeof(source), file); + bool bad = ferror(file) != 0; + if (fclose(file)) bad = true; + if (bad) { + if (error_size) snprintf(error, error_size, "cannot read program"); + return false; + } + return pure_program_parse(source, length, out, error, error_size); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.h b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.h new file mode 100644 index 0000000..c102eb1 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/program.h @@ -0,0 +1,44 @@ +#ifndef PURE_PROGRAM_H +#define PURE_PROGRAM_H +#include "flow.h" + +#define PURE_PROGRAM_LIMIT 16384u +#define PURE_DYNAMIC_LIMIT 8u +typedef struct { + size_t offset, length; + bool to_end; +} PureRegion; + +typedef struct { size_t value; bool from_u32le; } PureIndex; +typedef struct { + PureRegion source; + size_t source_step; /* 0: source region; otherwise a preceding step + 1. */ + PureIndex offset, length; +} PureRangeStep; + +/* Prepared connections, not an instruction stream. All view definitions are + * checked at preparation; their root-length requirements survive elimination. + * Input views are evaluated before the write effect. A read still opens and + * validates the object even if its response has no body. */ +typedef struct { + PureRegion read, write, reply; + size_t read_required, input_required; + size_t view_definitions, range_definitions; + size_t write_step, reply_step, input_steps; + PureRangeStep steps[PURE_DYNAMIC_LIMIT]; + bool reply_input; + bool splice; + PureIndex splice_offset, splice_remove; +} PureProgram; + +bool pure_program_parse(const char *source, size_t length, PureProgram *out, + char *error, size_t error_size); +bool pure_program_load(const char *path, PureProgram *out, char *error, size_t error_size); +bool pure_region_span(PureRegion region, PureSpan source, PureSpan *out); +/* Validates all input views, including unused dynamic views, before any effect. + * Returns borrowed selections; their lifetime remains that of source. */ +bool pure_program_input(const PureProgram *, PureSpan source, PureSpan *write, PureSpan *reply); +/* Byte positions in the stored value. Bounds against the current version are + * checked by the conditional storage effect under the writer lock. */ +bool pure_program_splice(const PureProgram *, PureSpan source, size_t *offset, size_t *remove); +#endif diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/result.h b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/result.h new file mode 100644 index 0000000..8abb0f2 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/result.h @@ -0,0 +1,8 @@ +#ifndef PURE_RESULT_H +#define PURE_RESULT_H +typedef enum { + PURE_OK, PURE_NOT_FOUND, PURE_CONFLICT, PURE_BUSY, PURE_BUDGET, + PURE_LIMIT, PURE_STORE_ERROR, PURE_UNCERTAIN, + PURE_INVALID, PURE_DENIED, PURE_CLOSED, PURE_CANCELLED, PURE_ALLOCATION +} PureResult; +#endif diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.c new file mode 100644 index 0000000..aa4ed05 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.c @@ -0,0 +1,273 @@ +#include "runtime.h" +#include <string.h> + +#define NONE SIZE_MAX +enum { FREE, RECEIVING, READY, RUNNING, DONE }; + +static PureWork *lookup(PureTicket ticket, bool require_held) { + PureRuntime *r = ticket.runtime; + if (!r || ticket.slot >= r->capacity || !ticket.generation) return NULL; + PureWork *w = &r->work[ticket.slot]; + if (w->state == FREE || w->generation != ticket.generation || + (require_held && !w->held)) return NULL; + return w; +} + +static bool valid_key(const char *key) { + if (!key || !*key) return false; + size_t n = 0; + for (; n <= 64 && key[n]; ++n) { + unsigned char c = (unsigned char)key[n]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '_')) return false; + } + return n <= 64; +} + +static void dispose(PureResource *resource) { + PureResource old = *resource; + *resource = (PureResource){0}; + if (old.release) old.release(old.allocation, old.allocation_size); +} + +static void payload_release(PureRuntime *r, PureWork *w); + +/* Iteratively retire a chain of forwarded values. An arbitrarily long chain + * must not turn completion into recursive stack growth. */ +static void retire(PureRuntime *r, PureWork *w) { + while (w && !w->held && !w->pins) { + PureTicket parent = w->parent; + bool owned = w->charged || w->resource.release || parent.runtime; + dispose(&w->resource); + r->retained -= w->charged; + if (owned) ++r->releases; + size_t slot = (size_t)(w - r->work); + *w = (PureWork){.next = r->free_head}; + r->free_head = slot; + --r->active; + if (!parent.runtime) break; + r = parent.runtime; + w = lookup(parent, false); + /* Pins can only be created and removed by the owner-thread core. */ + if (!w || !w->pins) break; + --w->pins; + } +} + +static void payload_release(PureRuntime *r, PureWork *w) { + PureTicket parent = w->parent; + bool owned = w->charged || w->resource.release || parent.runtime; + dispose(&w->resource); + r->retained -= w->charged; + w->charged = 0; + w->parent = (PureTicket){0}; + if (owned) ++r->releases; + if (parent.runtime) { + PureWork *source = lookup(parent, false); + if (source && source->pins) { --source->pins; retire(parent.runtime, source); } + } +} + +bool pure_runtime_init(PureRuntime *r, PureWork *work, size_t capacity, + const PureProgram *program, PureHost host, size_t budget) { + if (!r || !work || !capacity || !program) return false; + *r = (PureRuntime){.program = program, .host = host, .work = work, + .capacity = capacity, .free_head = 0, .ready_head = NONE, .ready_tail = NONE, + .budget = budget, .accepting = true}; + for (size_t i = 0; i < capacity; ++i) + work[i] = (PureWork){.next = i + 1 < capacity ? i + 1 : NONE}; + return true; +} + +bool pure_runtime_restart(PureRuntime *r, const PureProgram *p, PureHost host, size_t budget) { + if (!r || !p || r->running || r->active) return false; + r->program = p; + r->host = host; + r->budget = budget; + r->accepting = true; + return true; +} + +void pure_runtime_stop(PureRuntime *r) { if (r && !r->running) r->accepting = false; } + +static PureResult admission(PureRuntime *r, PureInput input, const char *key, + int64_t expected, size_t bytes) { + if (!r || r->running || !valid_key(key) || + (input != PURE_INPUT_READ && input != PURE_INPUT_WRITE) || + (input == PURE_INPUT_READ ? bytes != 0 : expected < 0)) return PURE_INVALID; + if (!r->accepting) return PURE_CLOSED; + if (input == PURE_INPUT_READ ? !r->host.read : + (r->program->splice ? !r->host.splice : !r->host.write)) return PURE_DENIED; + if (r->free_head == NONE) return PURE_BUSY; + if (r->serial == UINT64_MAX) return PURE_LIMIT; + if (bytes > r->budget - r->retained) { ++r->budget_rejections; return PURE_BUDGET; } + return PURE_OK; +} + +static PureTicket occupy(PureRuntime *r, PureInput input, const char *key, + int64_t expected, size_t bytes, PureResource resource) { + size_t slot = r->free_head; + PureWork *w = &r->work[slot]; + r->free_head = w->next; + *w = (PureWork){.generation = ++r->serial, .next = NONE, .charged = bytes, + .state = RECEIVING, .held = true, .input = input, .expected = expected, + .resource = resource}; + memcpy(w->key, key, strlen(key) + 1); + ++r->active; + ++r->accepted; + r->retained += bytes; + if (r->active > r->peak_active) r->peak_active = r->active; + if (r->retained > r->peak_retained) r->peak_retained = r->retained; + return (PureTicket){r, slot, w->generation}; +} + +bool pure_runtime_ready(PureTicket ticket) { + PureWork *w = lookup(ticket, true); + if (!w || ticket.runtime->running || w->state != RECEIVING) return false; + PureRuntime *r = ticket.runtime; + w->state = READY; + if (r->ready_tail == NONE) r->ready_head = ticket.slot; + else r->work[r->ready_tail].next = ticket.slot; + r->ready_tail = ticket.slot; + return true; +} + +PureResult pure_runtime_accept(PureRuntime *r, PureInput input, const char *key, + int64_t expected, size_t size, PureTicket *out) { + if (!out) return PURE_INVALID; + PureResult status = admission(r, input, key, expected, size); + if (status != PURE_OK) return status; + PureResource resource = {0}; + if (size) { + if (!r->host.allocate) return PURE_ALLOCATION; + r->running = true; + bool ok = r->host.allocate(r->host.context, size, &resource); + r->running = false; + if (!ok || !resource.body.data || resource.body.size != size || !resource.release) { + dispose(&resource); + return PURE_ALLOCATION; + } + } + *out = occupy(r, input, key, expected, size, resource); + if (input == PURE_INPUT_READ) pure_runtime_ready(*out); + return PURE_OK; +} + +unsigned char *pure_runtime_ingress(PureTicket ticket, size_t *size) { + PureWork *w = lookup(ticket, true); + if (size) *size = 0; + if (!w || ticket.runtime->running || w->state != RECEIVING || + w->input != PURE_INPUT_WRITE || w->parent.runtime) return NULL; + if (size) *size = w->resource.body.size; + return (unsigned char *)w->resource.body.data; +} + +bool pure_runtime_step(PureRuntime *r) { + if (!r || r->running || r->ready_head == NONE) return false; + size_t slot = r->ready_head; + PureWork *w = &r->work[slot]; + r->ready_head = w->next; + if (r->ready_head == NONE) r->ready_tail = NONE; + w->next = NONE; + w->state = RUNNING; + r->running = true; + PureOutcome outcome = {0}; + if (w->input == PURE_INPUT_WRITE) { + PureSpan value; + size_t offset = 0, remove = 0; + if (!pure_program_input(r->program, w->resource.body, &value, &outcome.body)) + outcome.status = PURE_LIMIT; + else if (r->program->splice) { + if (!pure_program_splice(r->program, w->resource.body, &offset, &remove)) + outcome.status = PURE_LIMIT; + else outcome.status = r->host.splice(r->host.context, w->key, w->expected, + offset, remove, value, &outcome.revision); + } + else outcome.status = r->host.write(r->host.context, w->key, w->expected, + value, &outcome.revision); + if (outcome.status == PURE_OK) ++r->commits; + else if (outcome.status == PURE_CONFLICT) ++r->conflicts; + } else { + outcome.status = r->host.read(r->host.context, w->key, r->program->read, + r->program->read_required, r->budget - r->retained, &w->resource, &outcome.revision); + if (outcome.status == PURE_OK) { + if (w->resource.body.size > r->budget - r->retained) outcome.status = PURE_BUDGET; + else if (w->resource.body.size && (!w->resource.body.data || !w->resource.release)) + outcome.status = PURE_STORE_ERROR; + else { + w->charged = w->resource.body.size; + r->retained += w->charged; + if (r->retained > r->peak_retained) r->peak_retained = r->retained; + outcome.body = w->resource.body; + ++r->gets; + } + } + if (outcome.status == PURE_BUDGET) ++r->budget_rejections; + } + if (outcome.status != PURE_OK || !outcome.body.size) { + outcome.body = (PureSpan){0}; + payload_release(r, w); + } + r->borrowed_output_bytes += outcome.body.size; + w->outcome = outcome; + w->state = DONE; + r->running = false; + return true; +} + +bool pure_runtime_result(PureTicket ticket, PureOutcome *out) { + PureWork *w = lookup(ticket, true); + if (!out || !w || ticket.runtime->running || w->state != DONE) return false; + *out = w->outcome; + return true; +} + +PureResult pure_runtime_forward(PureTicket source, PureRuntime *r, + const char *key, int64_t expected, PureTicket *out) { + PureWork *parent = lookup(source, true); + if (!out || !parent || source.runtime->running || parent->state != DONE || + parent->outcome.status != PURE_OK) return PURE_INVALID; + PureResult status = admission(r, PURE_INPUT_WRITE, key, expected, parent->outcome.body.size); + if (status != PURE_OK) return status; + if (parent->pins == SIZE_MAX) return PURE_LIMIT; + PureTicket ticket = occupy(r, PURE_INPUT_WRITE, key, expected, parent->outcome.body.size, + (PureResource){.body = parent->outcome.body}); + ++parent->pins; + r->work[ticket.slot].parent = source; + pure_runtime_ready(ticket); + *out = ticket; + return PURE_OK; +} + +static void unlink_ready(PureRuntime *r, size_t slot) { + size_t previous = NONE; + for (size_t at = r->ready_head; at != NONE; at = r->work[at].next) { + if (at == slot) { + if (previous == NONE) r->ready_head = r->work[at].next; + else r->work[previous].next = r->work[at].next; + if (r->ready_tail == at) r->ready_tail = previous; + r->work[at].next = NONE; + return; + } + previous = at; + } +} + +bool pure_runtime_cancel(PureTicket ticket) { + PureWork *w = lookup(ticket, true); + if (!w || ticket.runtime->running || (w->state != RECEIVING && w->state != READY)) return false; + if (w->state == READY) unlink_ready(ticket.runtime, ticket.slot); + payload_release(ticket.runtime, w); + w->outcome = (PureOutcome){.status = PURE_CANCELLED}; + w->state = DONE; + return true; +} + +bool pure_runtime_release(PureTicket ticket) { + PureWork *w = lookup(ticket, true); + if (!w || ticket.runtime->running) return false; + if (w->state == READY) unlink_ready(ticket.runtime, ticket.slot); + w->held = false; + retire(ticket.runtime, w); + return true; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.h b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.h new file mode 100644 index 0000000..1bff082 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/runtime.h @@ -0,0 +1,88 @@ +#ifndef PURE_RUNTIME_H +#define PURE_RUNTIME_H +#include "program.h" +#include "result.h" +#include <stdint.h> + +/* First execution profile: authored selection -> read/conditional write -> + * reply. No socket, file descriptor, allocator, parser or OS type in the core. + * All control calls and connected domains share one owner thread. Host effects + * are synchronous in this profile and must not reenter the runtime. */ +typedef struct PureRuntime PureRuntime; +typedef struct PureWork PureWork; +typedef struct { PureRuntime *runtime; size_t slot; uint64_t generation; } PureTicket; +typedef enum { PURE_INPUT_READ, PURE_INPUT_WRITE } PureInput; + +/* The host owns this resource until it successfully returns it to the core. + * release must end all access and free/unmap allocation exactly once. */ +typedef struct { + PureSpan body; + void *allocation; + size_t allocation_size; + void (*release)(void *allocation, size_t size); +} PureResource; +typedef struct { + void *context; + bool (*allocate)(void *, size_t, PureResource *); + PureResult (*read)(void *, const char *, PureRegion, size_t minimum, + size_t available, PureResource *, int64_t *revision); + PureResult (*write)(void *, const char *, int64_t expected, + PureSpan, int64_t *revision); + PureResult (*splice)(void *, const char *, int64_t expected, size_t offset, + size_t remove, PureSpan insert, int64_t *revision); +} PureHost; +typedef struct { PureResult status; int64_t revision; PureSpan body; } PureOutcome; + +/* Caller-provided work slots: no background thread or per-domain heap. These + * records are implementation storage, not an ABI or a wire representation. */ +struct PureWork { + uint64_t generation; + size_t next, charged, pins; + unsigned state; + bool held; + PureInput input; + int64_t expected; + char key[65]; + PureResource resource; + PureTicket parent; + PureOutcome outcome; +}; +struct PureRuntime { + const PureProgram *program; + PureHost host; + PureWork *work; + size_t capacity, free_head, ready_head, ready_tail; + size_t budget, retained, peak_retained, active, peak_active; + uint64_t serial, accepted, commits, gets, conflicts, budget_rejections; + uint64_t releases, borrowed_output_bytes; + bool accepting, running; +}; + +/* Initialize fresh storage once. Use restart, not init, to reuse a domain; + * the monotonic ticket sequence then survives restart. program is immutable + * and remains alive until all work has drained. */ +bool pure_runtime_init(PureRuntime *, PureWork *, size_t capacity, + const PureProgram *, PureHost, size_t budget); +bool pure_runtime_restart(PureRuntime *, const PureProgram *, PureHost, size_t budget); +void pure_runtime_stop(PureRuntime *); /* stop admission, allow queued work to drain */ + +/* READ queues immediately. WRITE reserves its slot, budget and host allocation + * before returning. Fill ingress then publish once with ready. A partial input + * released without ready never executes. On error, out is unchanged. */ +PureResult pure_runtime_accept(PureRuntime *, PureInput, const char *key, + int64_t expected, size_t input_size, PureTicket *out); +unsigned char *pure_runtime_ingress(PureTicket, size_t *size); +bool pure_runtime_ready(PureTicket); +bool pure_runtime_step(PureRuntime *); /* execute at most one ready job */ +bool pure_runtime_result(PureTicket, PureOutcome *out); + +/* Pin a completed reply and queue it as another domain's WRITE input, with no + * allocation, serialization or payload copy. Source may be released as soon + * as this succeeds; the core retains its backing until the consumer ends. + * Each domain charges its own retained input bytes as admission credits. + * Credits across domains are NOT distinct physical memory and must not be summed. */ +PureResult pure_runtime_forward(PureTicket source, PureRuntime *destination, + const char *key, int64_t expected, PureTicket *out); +bool pure_runtime_cancel(PureTicket); /* only before execution; keeps a completion */ +bool pure_runtime_release(PureTicket); /* invalidates caller's ticket, preserves pins */ +#endif diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/server.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/server.c new file mode 100644 index 0000000..50480c0 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/server.c @@ -0,0 +1,372 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include "program.h" +#include "store-port.h" +#include <arpa/inet.h> +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/socket.h> +#include <time.h> +#include <unistd.h> + +enum { SLOTS = 8, HEADER_LIMIT = 4096, CHUNK = 65536 }; +typedef enum { READING_HEADER, READING_BODY, WRITING } Phase; +typedef struct Server Server; +typedef struct { + int fd; + Phase phase; + uint64_t deadline; + char request[HEADER_LIMIT + 1], response[512], key[65]; + size_t header_size, body_size, received; + unsigned char *body; + bool put; + int64_t expected; + PureTicket ticket; + PureTransfer transfer; + Server *server; +} Connection; +struct Server { + const char *path; + PureProgram program; + PureRuntime runtime; + PureWork work[SLOTS]; + size_t budget, active, peak_active; + uint64_t accepted, expired, prefix_copy_bytes; + uint64_t timeout_ms; + int send_buffer; + Connection connections[SLOTS]; +}; +static volatile sig_atomic_t stopping; +static void stop_server(int sig) { (void)sig; stopping = 1; } + +static uint64_t now_ms(void) { + struct timespec t; + if (clock_gettime(CLOCK_MONOTONIC, &t)) abort(); + return (uint64_t)t.tv_sec * 1000 + (uint64_t)t.tv_nsec / 1000000; +} + +static bool number(const char *text, uint64_t maximum, uint64_t *value) { + if (!*text) return false; + uint64_t n = 0; + for (; *text; ++text) { + if (*text < '0' || *text > '9') return false; + unsigned digit = (unsigned)(*text - '0'); + if (digit > maximum || n > (maximum - digit) / 10) return false; + n = n * 10 + digit; + } + *value = n; + return true; +} + +static void release_payload(void *owner) { + Connection *c = owner; + if (c->ticket.runtime) { + bool released = pure_runtime_release(c->ticket); + assert(released); + (void)released; + } + c->ticket = (PureTicket){0}; + c->body = NULL; +} + +static void close_connection(Connection *c) { + if (c->fd < 0) return; + if (c->transfer.active) pure_transfer_abort(&c->transfer); + else release_payload(c); + close(c->fd); + --c->server->active; + *c = (Connection){.fd = -1}; +} + +static void respond(Connection *c, int status, const char *reason, const char *error, + int64_t revision, PureSpan body) { + char tag[80] = ""; + if (revision > 0) snprintf(tag, sizeof(tag), "ETag: \"%" PRId64 "\"\r\n", revision); + int length = snprintf(c->response, sizeof(c->response), + "HTTP/1.1 %d %s\r\nContent-Length: %zu\r\nConnection: close\r\n" + "Content-Type: application/octet-stream\r\nCache-Control: no-store\r\n" + "X-Pure-Error: %s\r\n%s\r\n", status, reason, body.size, error, tag); + assert(length > 0 && (size_t)length < sizeof(c->response)); + c->phase = WRITING; + pure_transfer_init(&c->transfer, + (PureSpan){(const unsigned char *)c->response, (size_t)length}, body, c, release_payload); +} + +static void fail(Connection *c, int status, const char *reason, const char *error) { + /* No outstanding backend access on the owner thread at this point. */ + release_payload(c); + respond(c, status, reason, error, 0, (PureSpan){0}); +} + +static void answer(Connection *c, PureOutcome result) { + switch (result.status) { + case PURE_OK: respond(c, 200, "OK", "none", result.revision, result.body); break; + case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break; + case PURE_CONFLICT: + respond(c, 412, "Precondition Failed", "conflict", result.revision, (PureSpan){0}); break; + case PURE_BUSY: fail(c, 503, "Service Unavailable", "store_busy"); break; + case PURE_BUDGET: fail(c, 503, "Service Unavailable", "payload_budget"); break; + case PURE_ALLOCATION: fail(c, 503, "Service Unavailable", "allocation"); break; + case PURE_CLOSED: fail(c, 503, "Service Unavailable", "stopped"); break; + case PURE_DENIED: fail(c, 403, "Forbidden", "denied"); break; + case PURE_LIMIT: fail(c, 413, "Content Too Large", "limit"); break; + case PURE_UNCERTAIN: fail(c, 500, "Internal Server Error", "durability_unknown"); break; + default: fail(c, 500, "Internal Server Error", "storage"); break; + } +} + +static void run_request(Connection *c) { + if (c->put && !pure_runtime_ready(c->ticket)) abort(); + PureOutcome result; + while (!pure_runtime_result(c->ticket, &result)) + if (!pure_runtime_step(&c->server->runtime)) abort(); + c->body = NULL; /* Core now owns input until all consumers release it. */ + answer(c, result); +} + +/* Deliberately bounded HTTP/1.1 subset: one GET/PUT, Content-Length, close. + * No transfer coding, pipelining, trailers, Expect, URL decoding, or proxying. */ +static bool parse_header(Connection *c, size_t end) { + for (size_t i = 0; i < end; ++i) { + unsigned char b = (unsigned char)c->request[i]; + if ((b < 32 && b != '\r' && b != '\n' && b != '\t') || b == 127) return false; + } + char *line = c->request, *next = strstr(line, "\r\n"); + if (!next) return false; + *next = 0; + char *path = strchr(line, ' '); + if (!path) return false; + *path++ = 0; + if (strcmp(line, "PUT") == 0) c->put = true; + else if (strcmp(line, "GET") != 0) return false; + char *version = strchr(path, ' '); + if (!version) return false; + *version++ = 0; + if (strcmp(version, "HTTP/1.1") || strncmp(path, "/objects/", 9)) return false; + const char *key = path + 9; + size_t key_size = strlen(key); + if (!key_size || key_size > 64) return false; + for (size_t i = 0; i < key_size; ++i) { + char b = key[i]; + if (!((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || + (b >= '0' && b <= '9') || b == '-' || b == '_')) return false; + } + memcpy(c->key, key, key_size + 1); + bool length_seen = false, condition_seen = false, host_seen = false; + line = next + 2; + while (*line != '\r') { + next = strstr(line, "\r\n"); + if (!next) return false; + *next = 0; + char *value = strchr(line, ':'); + if (!value || value == line) return false; + *value++ = 0; + for (char *p = line; *p; ++p) + if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '-')) return false; + while (*value == ' ' || *value == '\t') ++value; + char *tail = value + strlen(value); + while (tail > value && (tail[-1] == ' ' || tail[-1] == '\t')) *--tail = 0; + if (strchr(value, '\r') || strchr(value, '\n')) return false; + uint64_t n; + if (!strcasecmp(line, "Content-Length")) { + if (length_seen || !number(value, SIZE_MAX, &n)) return false; + length_seen = true; + c->body_size = (size_t)n; + } 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, "Transfer-Encoding") || !strcasecmp(line, "Expect")) return false; + line = next + 2; + } + return host_seen && (c->put ? length_seen && condition_seen : !condition_seen && c->body_size == 0); +} + +static void read_connection(Connection *c) { + if (c->phase == READING_HEADER) { + ssize_t n = recv(c->fd, c->request + c->header_size, HEADER_LIMIT - c->header_size, 0); + if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; } + if (!n) { close_connection(c); return; } + c->header_size += (size_t)n; + c->request[c->header_size] = 0; + /* Scan by byte count, so embedded NUL cannot hide a terminator. */ + size_t end = 0; + for (size_t i = 3; i < c->header_size; ++i) + if (!memcmp(c->request + i - 3, "\r\n\r\n", 4)) { end = i + 1; break; } + if (!end) { + if (c->header_size == HEADER_LIMIT) fail(c, 431, "Request Header Fields Too Large", "headers"); + return; + } + size_t prefix = c->header_size - end; + if (!parse_header(c, end)) { fail(c, 400, "Bad Request", "request"); return; } + if (c->body_size > PURE_OBJECT_LIMIT) { fail(c, 413, "Content Too Large", "limit"); return; } + if (prefix > c->body_size) { fail(c, 400, "Bad Request", "extra_body"); return; } + PureResult admitted = pure_runtime_accept(&c->server->runtime, + c->put ? PURE_INPUT_WRITE : PURE_INPUT_READ, c->key, c->expected, c->body_size, &c->ticket); + if (admitted != PURE_OK) { answer(c, (PureOutcome){.status = admitted}); return; } + if (!c->put) { run_request(c); return; } + c->body = pure_runtime_ingress(c->ticket, NULL); + if (prefix) memcpy(c->body, c->request + end, prefix); + c->server->prefix_copy_bytes += prefix; + c->received = prefix; + c->phase = READING_BODY; + if (c->received == c->body_size) run_request(c); + } else { + size_t remaining = c->body_size - c->received; + ssize_t n = recv(c->fd, c->body + c->received, remaining < CHUNK ? remaining : CHUNK, 0); + if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; } + if (!n) { close_connection(c); return; } + c->received += (size_t)n; + if (c->received == c->body_size) run_request(c); + } +} + +static void write_connection(Connection *c) { + PureSpan span = pure_transfer_next(&c->transfer, CHUNK); + ssize_t n = send(c->fd, span.data, span.size, 0); + if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; } + if (!n) { close_connection(c); return; } + if (!pure_transfer_consume(&c->transfer, (size_t)n)) abort(); + if (!c->transfer.active) close_connection(c); +} + +static bool nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0; +} + +int main(int argc, char **argv) { + Server server = {.budget = 4u * 1024u * 1024u, .timeout_ms = 10000}; + unsigned port = 8767; + const char *program_path = NULL; + bool explain = argc == 3 && !strcmp(argv[1], "--check-program"); + char program_error[160]; + if (explain) program_path = argv[2]; + for (int i = 1; !explain && i < argc; i += 2) { + if (i + 1 == argc) goto usage; + uint64_t value; + if (!strcmp(argv[i], "--store")) server.path = argv[i + 1]; + else if (!strcmp(argv[i], "--program")) program_path = argv[i + 1]; + else if (!strcmp(argv[i], "--port") && number(argv[i + 1], 65535, &value)) port = (unsigned)value; + else if (!strcmp(argv[i], "--budget") && number(argv[i + 1], SIZE_MAX, &value) && value) server.budget = (size_t)value; + else if (!strcmp(argv[i], "--timeout-ms") && number(argv[i + 1], 3600000, &value) && value) server.timeout_ms = value; + else if (!strcmp(argv[i], "--send-buffer") && number(argv[i + 1], INT32_MAX, &value)) server.send_buffer = (int)value; + else goto usage; + } + if (!program_path || (!explain && !server.path)) goto usage; + if (!pure_program_load(program_path, &server.program, program_error, sizeof(program_error))) { + fprintf(stderr, "Program: %s\n", program_error); return 2; + } + if (explain) { + PureProgram *p = &server.program; + printf("{\"read\":{\"to_end\":%s,\"offset\":%zu,\"length\":%zu,\"minimum_source_bytes\":%zu}," + "\"write\":{\"to_end\":%s,\"offset\":%zu,\"length\":%zu,\"minimum_source_bytes\":%zu}," + "\"reply_input\":%s,\"view_definitions\":%zu,\"range_definitions\":%zu," + "\"runtime_view_steps\":%zu,\"write_step\":%zu,\"reply_step\":%zu,\"prepared_program_bytes\":%zu,\"steps\":[", + p->read.to_end ? "true" : "false", p->read.offset, p->read.length, p->read_required, + p->write.to_end ? "true" : "false", p->write.offset, p->write.length, p->input_required, + p->reply_input ? "true" : "false", p->view_definitions, p->range_definitions, + p->input_steps, p->write_step, p->reply_step, sizeof(*p)); + for (size_t i = 0; i < p->input_steps; ++i) { + const PureRangeStep *step = &p->steps[i]; + printf("%s{\"source_step\":%zu,\"source\":{\"offset\":%zu,\"length\":%zu,\"to_end\":%s}," + "\"offset\":{\"value\":%zu,\"from_u32le\":%s},\"length\":{\"value\":%zu,\"from_u32le\":%s}}", + i ? "," : "", step->source_step, step->source.offset, step->source.length, + step->source.to_end ? "true" : "false", step->offset.value, step->offset.from_u32le ? "true" : "false", + step->length.value, step->length.from_u32le ? "true" : "false"); + } + printf("],\"write_effect\":\"%s\",\"splice_offset\":{\"value\":%zu,\"from_u32le\":%s}," + "\"splice_remove\":{\"value\":%zu,\"from_u32le\":%s}}\n", + p->splice ? "splice" : "write", p->splice_offset.value, + p->splice_offset.from_u32le ? "true" : "false", p->splice_remove.value, + p->splice_remove.from_u32le ? "true" : "false"); + return 0; + } + if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize byte store directory\n", stderr); return 1; } + if (!pure_runtime_init(&server.runtime, server.work, SLOTS, &server.program, + pure_store_host(server.path), server.budget)) return 1; + signal(SIGPIPE, SIG_IGN); + signal(SIGINT, stop_server); + signal(SIGTERM, stop_server); + int listener = socket(AF_INET, SOCK_STREAM, 0); + if (listener < 0) { perror("socket"); return 1; } + int one = 1; + setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK)}; + if (bind(listener, (struct sockaddr *)&address, sizeof(address)) || listen(listener, SLOTS) || !nonblocking(listener)) { + perror("listen"); close(listener); return 1; + } + socklen_t address_size = sizeof(address); + if (getsockname(listener, (struct sockaddr *)&address, &address_size)) { close(listener); return 1; } + for (size_t i = 0; i < SLOTS; ++i) server.connections[i].fd = -1; + printf("{\"port\":%u,\"storage\":\"immutable-byte-extents-v1\"}\n", ntohs(address.sin_port)); + fflush(stdout); + while (!stopping) { + struct pollfd polls[SLOTS + 1] = {{.fd = server.active < SLOTS ? listener : -1, .events = POLLIN}}; + for (size_t i = 0; i < SLOTS; ++i) { + Connection *c = &server.connections[i]; + polls[i + 1] = (struct pollfd){.fd = c->fd, .events = c->phase == WRITING ? POLLOUT : POLLIN}; + } + int ready = poll(polls, SLOTS + 1, 50); + if (ready < 0 && errno != EINTR) { perror("poll"); stopping = 1; } + uint64_t now = now_ms(); + for (size_t i = 0; i < SLOTS; ++i) { + Connection *c = &server.connections[i]; + if (c->fd < 0) continue; + if (now >= c->deadline) { ++server.expired; close_connection(c); continue; } + short events = polls[i + 1].revents; + if (events & (POLLERR | POLLNVAL)) { close_connection(c); continue; } + if (events & (POLLIN | POLLOUT | POLLHUP)) { + if (c->phase == WRITING) write_connection(c); else read_connection(c); + } + } + if (!stopping && (polls[0].revents & POLLIN)) { + int fd = accept(listener, NULL, NULL); + if (fd < 0) continue; + if (!nonblocking(fd)) { close(fd); continue; } + if (server.send_buffer) setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &server.send_buffer, sizeof(server.send_buffer)); + for (size_t i = 0; i < SLOTS; ++i) if (server.connections[i].fd < 0) { + server.connections[i] = (Connection){.fd = fd, .server = &server, .deadline = now_ms() + server.timeout_ms}; + ++server.active; + ++server.accepted; + if (server.active > server.peak_active) server.peak_active = server.active; + break; + } + } + } + close(listener); + pure_runtime_stop(&server.runtime); + for (size_t i = 0; i < SLOTS; ++i) close_connection(&server.connections[i]); + fprintf(stderr, "{\"active\":%zu,\"retained_payload_bytes\":%zu,\"peak_retained_payload_bytes\":%zu," + "\"payload_budget\":%zu,\"peak_connections\":%zu,\"accepted\":%" PRIu64 ",\"commits\":%" PRIu64 "," + "\"gets\":%" PRIu64 ",\"conflicts\":%" PRIu64 ",\"budget_rejections\":%" PRIu64 "," + "\"expired\":%" PRIu64 ",\"releases\":%" PRIu64 ",\"ingress_prefix_copy_bytes\":%" PRIu64 "," + "\"borrowed_output_bytes\":%" PRIu64 ",\"server_structure_bytes\":%zu}\n", + server.active, server.runtime.retained, server.runtime.peak_retained, server.budget, server.peak_active, + server.accepted, server.runtime.commits, server.runtime.gets, server.runtime.conflicts, server.runtime.budget_rejections, + server.expired, server.runtime.releases, server.prefix_copy_bytes, server.runtime.borrowed_output_bytes, sizeof(server)); + return 0; +usage: + fputs("pure-connections --program FILE --store DIRECTORY [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n" + "pure-connections --check-program FILE\n", stderr); + return 2; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.c new file mode 100644 index 0000000..9860308 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.c @@ -0,0 +1,44 @@ +#include "store-port.h" +#include "store.h" +#include <stdlib.h> +#include <sys/mman.h> + +static void free_memory(void *data, size_t size) { (void)size; free(data); } +static void unmap_memory(void *data, size_t size) { if (size) munmap(data, size); } + +static bool allocate(void *context, size_t size, PureResource *out) { + (void)context; + void *data = malloc(size); + if (!data) return false; + *out = (PureResource){.body = {data, size}, .allocation = data, + .allocation_size = size, .release = free_memory}; + return true; +} + +static PureResult read_value(void *context, const char *key, PureRegion region, + size_t minimum, size_t available, PureResource *out, int64_t *revision) { + PureStoredView view = {0}; + PureResult result = pure_store_select(context, key, region.offset, region.length, + region.to_end, minimum, available, &view); + if (result == PURE_OK) { + *out = (PureResource){.body = view.body, .allocation = view.mapping, + .allocation_size = view.mapped_size, .release = view.mapped_size ? unmap_memory : NULL}; + *revision = view.revision; + } + return result; +} + +static PureResult write_value(void *context, const char *key, int64_t expected, + PureSpan body, int64_t *revision) { + return pure_store_put(context, key, expected, body, revision); +} + +static PureResult splice_value(void *context, const char *key, int64_t expected, + size_t offset, size_t remove, PureSpan insert, int64_t *revision) { + return pure_store_splice(context, key, expected, offset, remove, insert, revision); +} + +PureHost pure_store_host(const char *path) { + return (PureHost){.context = (void *)path, .allocate = allocate, + .read = read_value, .write = write_value, .splice = splice_value}; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.h b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.h new file mode 100644 index 0000000..4c0d49d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store-port.h @@ -0,0 +1,6 @@ +#ifndef PURE_STORE_PORT_H +#define PURE_STORE_PORT_H +#include "runtime.h" +/* The path remains alive for the domain's complete lifetime, including pins. */ +PureHost pure_store_host(const char *path); +#endif diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.c new file mode 100644 index 0000000..4c8bdc4 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.c @@ -0,0 +1,258 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <unistd.h> + +/* Immutable extent envelope: marker, little-endian u64 revision and payload + * length, then unchanged caller bytes. Single-threaded control API; cooperating + * processes serialize publication using .writer.lock. */ +enum { HEADER_BYTES = 24 }; +static const unsigned char magic[8] = {'P','U','R','E','B','0','0','1'}; +static uint64_t temporary_sequence; + +static bool fault(PureStoreStage stage) { +#ifdef PURE_STORE_TEST + if (pure_store_test_fault(stage)) { errno = EIO; return true; } +#else + (void)stage; +#endif + return false; +} + +static bool file_name(const char *key, char name[72]) { + if (!key) return false; + size_t length = strlen(key); + if (!length || length > 64) return false; + for (size_t i = 0; i < length; ++i) { + char b = key[i]; + if (!((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || + (b >= '0' && b <= '9') || b == '-' || b == '_')) return false; + } + snprintf(name, 72, "obj-%s", key); + return true; +} + +static uint64_t decode64(const unsigned char *bytes) { + uint64_t value = 0; + for (unsigned i = 0; i < 8; ++i) value |= (uint64_t)bytes[i] << (8 * i); + return value; +} + +static void encode64(unsigned char *bytes, uint64_t value) { + for (unsigned i = 0; i < 8; ++i) bytes[i] = (unsigned char)(value >> (8 * i)); +} + +static bool sync_fd(int fd) { + int rc; + do { rc = fsync(fd); } while (rc && errno == EINTR); + return rc == 0; +} + +static bool write_bytes(int fd, const unsigned char *bytes, size_t length) { + while (length) { + ssize_t n = write(fd, bytes, length); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + bytes += (size_t)n; + length -= (size_t)n; + } + return true; +} + +static bool read_header(int fd, int64_t *revision, size_t *length) { + struct stat info; + if (fstat(fd, &info) || !S_ISREG(info.st_mode)) return false; + unsigned char header[HEADER_BYTES]; + size_t used = 0; + while (used < sizeof(header)) { + ssize_t n = pread(fd, header + used, sizeof(header) - used, (off_t)used); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + used += (size_t)n; + } + uint64_t version = decode64(header + 8), size = decode64(header + 16); + if (memcmp(header, magic, sizeof(magic)) || !version || version > INT64_MAX || + size > PURE_OBJECT_LIMIT || info.st_size != (off_t)(HEADER_BYTES + size)) return false; + *revision = (int64_t)version; + *length = (size_t)size; + return true; +} + +PureResult pure_store_init(const char *path) { + if (!path || !*path) return PURE_STORE_ERROR; + bool created = mkdir(path, 0700) == 0; + if (!created && errno != EEXIST) return PURE_STORE_ERROR; + int directory = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (directory < 0) return PURE_STORE_ERROR; /* Never reinterpret an old DB file. */ + int lock = openat(directory, ".writer.lock", O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, 0600); + struct stat info; + bool ok = lock >= 0 && fstat(lock, &info) == 0 && S_ISREG(info.st_mode) && sync_fd(lock); + if (lock >= 0) close(lock); + if (ok) ok = sync_fd(directory); + close(directory); + if (!ok) return PURE_STORE_ERROR; + if (created) { + char *parent = strdup(path); + if (!parent) return PURE_STORE_ERROR; + size_t length = strlen(parent); + while (length > 1 && parent[length - 1] == '/') parent[--length] = 0; + char *slash = strrchr(parent, '/'); + if (!slash) strcpy(parent, "."); + else if (slash == parent) parent[1] = 0; + else *slash = 0; + int fd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + free(parent); + ok = fd >= 0 && sync_fd(fd); + if (fd >= 0) close(fd); + } + return ok ? PURE_OK : PURE_STORE_ERROR; +} + +void pure_store_release(PureStoredView *view) { + if (view->mapped_size) munmap(view->mapping, view->mapped_size); + *view = (PureStoredView){0}; +} + +PureResult pure_store_select(const char *path, const char *key, size_t offset, + size_t length, bool whole, size_t minimum, + size_t available, PureStoredView *out) { + *out = (PureStoredView){0}; + char name[72]; + if (!file_name(key, name)) return PURE_STORE_ERROR; + int directory = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (directory < 0) return PURE_STORE_ERROR; + int fd = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + int cause = errno; + close(directory); + if (fd < 0) return cause == ENOENT ? PURE_NOT_FOUND : PURE_STORE_ERROR; + int64_t revision; + size_t size; + PureResult result = PURE_STORE_ERROR; + if (!read_header(fd, &revision, &size)) goto finish; + if (size < minimum) { result = PURE_LIMIT; goto finish; } + if (offset > size) { result = PURE_LIMIT; goto finish; } + if (whole) length = size - offset; + if (offset > size || length > size - offset) { result = PURE_LIMIT; goto finish; } + if (length > available) { result = PURE_BUDGET; goto finish; } + if (length) { + long page = sysconf(_SC_PAGESIZE); + if (page <= 0) goto finish; + size_t start = HEADER_BYTES + offset; + size_t aligned = start - start % (size_t)page; + size_t prefix = start - aligned; + size_t mapped_size = prefix + length; + void *mapping = mmap(NULL, mapped_size, PROT_READ, MAP_PRIVATE, fd, (off_t)aligned); + if (mapping == MAP_FAILED) goto finish; + out->mapping = mapping; + out->mapped_size = mapped_size; + out->body = (PureSpan){(const unsigned char *)mapping + prefix, length}; + } + out->revision = revision; + out->object_size = size; + result = PURE_OK; +finish: + close(fd); /* The mapping retains this immutable file version after rename. */ + return result; +} + +PureResult pure_store_get(const char *path, const char *key, size_t available, PureStoredView *out) { + return pure_store_select(path, key, 0, 0, true, 0, available, out); +} + +PureResult pure_store_get_range(const char *path, const char *key, size_t offset, + size_t length, size_t available, PureStoredView *out) { + return pure_store_select(path, key, offset, length, false, 0, available, out); +} + +static bool copy_extent(int source, int destination, size_t offset, size_t length) { + unsigned char scratch[4096]; + while (length) { + size_t wanted = length < sizeof(scratch) ? length : sizeof(scratch); + ssize_t n = pread(source, scratch, wanted, (off_t)(HEADER_BYTES + offset)); + if (n < 0 && errno == EINTR) continue; + if (n <= 0 || !write_bytes(destination, scratch, (size_t)n)) return false; + offset += (size_t)n; + length -= (size_t)n; + } + return true; +} + +static PureResult mutate(const char *path, const char *key, int64_t expected, + PureSpan body, bool splice, size_t offset, size_t remove, int64_t *revision) { + *revision = 0; + char name[72], temporary[96] = ""; + if (!file_name(key, name) || (body.size && !body.data)) return PURE_STORE_ERROR; + if (body.size > PURE_OBJECT_LIMIT || expected < 0) return PURE_LIMIT; + int directory = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (directory < 0) return PURE_STORE_ERROR; + int lock = openat(directory, ".writer.lock", O_RDWR | O_NOFOLLOW | O_CLOEXEC); + int fd = -1, source = -1; + size_t old_size = 0, result_size = body.size; + PureResult result = PURE_STORE_ERROR; + struct stat info; + if (lock < 0 || fstat(lock, &info) || !S_ISREG(info.st_mode)) goto finish; + struct flock hold = {.l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 0}; + if (fcntl(lock, F_SETLK, &hold)) { + if (errno == EACCES || errno == EAGAIN) result = PURE_BUSY; + goto finish; + } + source = openat(directory, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); + if (source >= 0) { + if (!read_header(source, revision, &old_size)) goto finish; + } else if (errno != ENOENT) goto finish; + if (*revision != expected) { result = PURE_CONFLICT; goto finish; } + if (*revision == INT64_MAX) { result = PURE_LIMIT; goto finish; } + if (splice) { + if (offset > old_size || remove > old_size - offset || + body.size > PURE_OBJECT_LIMIT - (old_size - remove)) { result = PURE_LIMIT; goto finish; } + result_size = old_size - remove + body.size; + } + for (unsigned attempt = 0; attempt < 64; ++attempt) { + if (temporary_sequence == UINT64_MAX) goto finish; + snprintf(temporary, sizeof(temporary), ".pending-%ld-%" PRIu64, (long)getpid(), ++temporary_sequence); + fd = openat(directory, temporary, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd >= 0) break; + temporary[0] = 0; /* Never remove an existing file on a name collision. */ + if (errno != EEXIST) goto finish; + } + if (fd < 0) goto finish; + unsigned char header[HEADER_BYTES]; + memcpy(header, magic, sizeof(magic)); + encode64(header + 8, (uint64_t)*revision + 1); + encode64(header + 16, result_size); + if (!write_bytes(fd, header, sizeof(header)) || fault(PURE_AFTER_HEADER)) goto finish; + if (splice && !copy_extent(source, fd, 0, offset)) goto finish; + if (!write_bytes(fd, body.data, body.size)) goto finish; + if (splice && !copy_extent(source, fd, offset + remove, old_size - offset - remove)) goto finish; + if (fault(PURE_BEFORE_FILE_SYNC) || !sync_fd(fd)) goto finish; + int close_result = close(fd); + fd = -1; + if (close_result || fault(PURE_BEFORE_RENAME) || renameat(directory, temporary, directory, name)) goto finish; + temporary[0] = 0; /* Published: a later sync failure does not roll it back. */ + ++*revision; + result = fault(PURE_BEFORE_DIR_SYNC) || !sync_fd(directory) ? PURE_UNCERTAIN : PURE_OK; +finish: + if (fd >= 0) close(fd); + if (source >= 0) close(source); + if (*temporary) unlinkat(directory, temporary, 0); + if (lock >= 0) close(lock); + close(directory); + return result; +} + +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, int64_t *revision) { + return mutate(path, key, expected, body, false, 0, 0, revision); +} + +PureResult pure_store_splice(const char *path, const char *key, int64_t expected, + size_t offset, size_t remove, PureSpan insert, int64_t *revision) { + return mutate(path, key, expected, insert, true, offset, remove, revision); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.h b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.h new file mode 100644 index 0000000..99a3ef5 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/store.h @@ -0,0 +1,44 @@ +#ifndef PURE_STORE_H +#define PURE_STORE_H +#include "flow.h" +#include "result.h" +#include <stdint.h> + +#define PURE_OBJECT_LIMIT ((2u * 6u + 32u) * 32768u + 8192u) +typedef struct { + void *mapping; + size_t mapped_size, object_size; + PureSpan body; + int64_t revision; +} PureStoredView; + +PureResult pure_store_init(const char *path); +PureResult pure_store_get(const char *path, const char *key, size_t available, + PureStoredView *out); +PureResult pure_store_get_range(const char *path, const char *key, size_t offset, + size_t length, size_t available, PureStoredView *out); +/* Inspect once: guards and mapping refer to the same immutable file version. + * minimum preserves bounds checks from eliminated intermediate views. + * whole selects the suffix starting at offset; otherwise length is exact. */ +PureResult pure_store_select(const char *path, const char *key, size_t offset, + size_t length, bool whole, size_t minimum, + size_t available, PureStoredView *out); +void pure_store_release(PureStoredView *); +PureResult pure_store_put(const char *path, const char *key, int64_t expected, + PureSpan body, int64_t *revision); +/* Atomic byte replacement: retain [0,offset), insert bytes, retain suffix after + * remove bytes. Check the expected revision and bounds under the same writer + * lock. expected=0 permits creating an empty base (offset=remove=0). + * Streams retained bytes through bounded scratch space; never assembles a + * whole new object in application memory. Still rewrites the immutable file. */ +PureResult pure_store_splice(const char *path, const char *key, int64_t expected, + size_t offset, size_t remove, PureSpan insert, int64_t *revision); + +/* Fault hooks exist only in the separately built unit-test binary. */ +typedef enum { + PURE_AFTER_HEADER, PURE_BEFORE_FILE_SYNC, PURE_BEFORE_RENAME, PURE_BEFORE_DIR_SYNC +} PureStoreStage; +#ifdef PURE_STORE_TEST +int pure_store_test_fault(PureStoreStage stage); +#endif +#endif diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-fields.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-fields.c new file mode 100644 index 0000000..63dfc3e --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-fields.c @@ -0,0 +1,153 @@ +#include "program.h" +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#define CHECK(expr) do { if (!(expr)) { fprintf(stderr, "line %d: %s\n", __LINE__, #expr); exit(1); } } while (0) +static const char get[] = "on get { let o = store.read(request.key); reply(o, o.body); }"; +static const char put[] = "on put { let s = store.write(request.key, request.revision, request.body); reply(s); }"; +static const char layout[] = "layout Envelope { marker: bytes[1]; start: u32le; count: u32le; text: bytes; }"; +static PureProgram parse(const char *source) { + PureProgram p; + char error[160]; + if (!pure_program_parse(source, strlen(source), &p, error, sizeof(error))) { + fprintf(stderr, "%s\n", error); exit(1); + } + return p; +} +static void invalid(const char *source) { + PureProgram p; + memset(&p, 0x5a, sizeof(p)); + unsigned char before[sizeof(p)]; + memcpy(before, &p, sizeof(p)); + char error[160]; + CHECK(!pure_program_parse(source, strlen(source), &p, error, sizeof(error))); + CHECK(*error && !memcmp(before, &p, sizeof(p))); +} +static void encode(unsigned char *at, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) at[i] = (unsigned char)(value >> (8 * i)); +} +int main(void) { + char source[8000]; + snprintf(source, sizeof(source), "%s %s on put { let input = view(Envelope, request.body); " + "let alias = input; let start = alias.start; let text = alias.text; " + "let selected = range(text, start, alias.count); let result = selected; " + "let saved = store.write(request.key, request.revision, result); reply(saved, selected); }", layout, get); + PureProgram p = parse(source); + CHECK(p.input_steps == 1 && p.input_required == 9 && p.write_step == 1 && p.reply_step == 1); + CHECK(p.steps[0].source.offset == 9 && p.steps[0].source.to_end); + CHECK(p.steps[0].offset.value == 1 && p.steps[0].offset.from_u32le); + CHECK(p.steps[0].length.value == 5 && p.steps[0].length.from_u32le); + unsigned char bytes[609]; + for (size_t i = 0; i < sizeof(bytes); ++i) bytes[i] = (unsigned char)(i * 13); + PureSpan stored, returned; + for (size_t n = 0; n < 9; ++n) { + CHECK(!pure_program_input(&p, (PureSpan){bytes, n}, &stored, &returned)); + CHECK(!stored.size && !returned.size); + } + const uint32_t offsets[] = {0, 1, 255, 257, 599, 600, 601, UINT32_MAX}; + const uint32_t counts[] = {0, 1, 2, 260, 600, UINT32_MAX}; + size_t comparisons = 0; + for (size_t i = 0; i < sizeof(offsets) / sizeof(*offsets); ++i) + for (size_t j = 0; j < sizeof(counts) / sizeof(*counts); ++j) { + uint32_t start = offsets[i], count = counts[j]; + encode(bytes + 1, start); + encode(bytes + 5, count); + bool expected = start <= 600 && count <= 600 - start; + CHECK(pure_program_input(&p, (PureSpan){bytes, sizeof(bytes)}, &stored, &returned) == expected); + if (expected) { + CHECK(stored.size == count && returned.size == count && stored.data == returned.data); + CHECK(!count || stored.data == bytes + 9 + start); + } else CHECK(!stored.size && !returned.size); + ++comparisons; + } + snprintf(source, sizeof(source), "%s %s on put { let input = view(Envelope, request.body); " + "let first = range(input.text, input.start, input.count); let second = range(first, 1, 2); " + "let saved = store.write(request.key, request.revision, second); reply(saved, second); }", layout, get); + p = parse(source); + CHECK(p.input_steps == 2 && p.steps[1].source_step == 1); + encode(bytes + 1, 3); encode(bytes + 5, 4); + CHECK(pure_program_input(&p, (PureSpan){bytes, sizeof(bytes)}, &stored, &returned)); + CHECK(stored.size == 2 && stored.data == bytes + 13); + encode(bytes + 5, 2); + CHECK(!pure_program_input(&p, (PureSpan){bytes, sizeof(bytes)}, &stored, &returned)); + snprintf(source, sizeof(source), "%s %s on put { let input = view(Envelope, request.body); " + "let unused = range(input.text, input.start, input.count); " + "let saved = store.write(request.key, request.revision, input.text); reply(saved); }", layout, get); + p = parse(source); + encode(bytes + 1, UINT32_MAX); + CHECK(!pure_program_input(&p, (PureSpan){bytes, sizeof(bytes)}, &stored, &returned)); + + snprintf(source, sizeof(source), "%s on get { let o = store.read(request.key); " + "let input = view(Envelope, o.body); reply(o, input.text); } %s", layout, put); + p = parse(source); + CHECK(p.read.to_end && p.read.offset == 9 && p.read_required == 9 && !p.input_steps); + snprintf(source, sizeof(source), "%s %s on put { let input = view(Envelope, request.body); " + "let selection = range(input.text, 2, 3); let saved = store.write(request.key, request.revision, selection); reply(saved); }", layout, get); + p = parse(source); + CHECK(!p.input_steps && p.write.offset == 11 && p.write.length == 3 && p.input_required == 14); + snprintf(source, sizeof(source), "layout Empty { body: bytes; } %s on put { let input = view(Empty, request.body); " + "let saved = store.write(request.key, request.revision, input.body); reply(saved, input.body); }", get); + p = parse(source); + CHECK(pure_program_input(&p, (PureSpan){0}, &stored, &returned) && !stored.size && !returned.size); + + const char *bad_layout[] = { + "layout E {}", "layout E { x: bytes; y: u32le; }", "layout E { x: u32le; x: bytes; }", + "layout E { x: bytes[1449985]; }", "layout E { x: bytes[1449984]; y: u32le; }", + "layout E { x: bytes[184467440737095516160]; }", "layout E { x: u64le; }", + "layout E { x: u32le; } layout E { y: bytes; }" + }; + for (size_t i = 0; i < sizeof(bad_layout) / sizeof(*bad_layout); ++i) { + snprintf(source, sizeof(source), "%s %s %s", bad_layout[i], get, put); invalid(source); + } + const char *bad_input[] = { + "let input = view(Missing, request.body);", + "let input = view(Envelope, request.body); let x = input.missing;", + "let input = view(Envelope, request.body); let x = range(input.text, input.text, 1);", + "let short = range(request.body, 0, 8); let input = view(Envelope, short);", + "let input = view(Envelope, request.body); let x = range(input.text, input.start, 20); let y = view(Envelope, x);" + }; + for (size_t i = 0; i < sizeof(bad_input) / sizeof(*bad_input); ++i) { + snprintf(source, sizeof(source), "%s %s on put { %s let saved = store.write(request.key, request.revision, request.body); reply(saved); }", layout, get, bad_input[i]); + invalid(source); + } + snprintf(source, sizeof(source), "%s %s on put { let input = view(Envelope, request.body); " + "let saved = store.write(request.key, request.revision, input.start); reply(saved); }", layout, get); + invalid(source); + snprintf(source, sizeof(source), "%s on get { let o = store.read(request.key); let input = view(Envelope, o.body); " + "let x = range(input.text, input.start, input.count); reply(o, x); } %s", layout, put); + invalid(source); + for (size_t count = 8; count <= 9; ++count) { + snprintf(source, sizeof(source), "%s %s on put { let input = view(Envelope, request.body); ", layout, get); + for (size_t i = 0; i < count; ++i) { + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "let v%zu = range(input.text, input.start, input.count); ", i); + } + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "let saved = store.write(request.key, request.revision, input.text); reply(saved); }"); + if (count == 8) CHECK(parse(source).input_steps == 8); else invalid(source); + } + for (size_t count = 4; count <= 5; ++count) { + source[0] = 0; + for (size_t i = 0; i < count; ++i) { + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "layout L%zu { body: bytes; } ", i); + } + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "%s %s", get, put); + if (count == 4) (void)parse(source); else invalid(source); + } + for (size_t count = 16; count <= 17; ++count) { + strcpy(source, "layout L { "); + for (size_t i = 0; i < count; ++i) { + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "f%zu: u32le; ", i); + } + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "} %s %s", get, put); + if (count == 16) (void)parse(source); else invalid(source); + } + printf("{\"fields\":\"named-layouts-unaligned-u32le-tail-dynamic-ranges-aliases-failures-limits\"," + "\"data_boundary_cases\":%zu}\n", comparisons); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-flow.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-flow.c new file mode 100644 index 0000000..5ff028d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-flow.c @@ -0,0 +1,183 @@ +#define _POSIX_C_SOURCE 200809L +#include "store.h" +#include <dirent.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unistd.h> + +#define CHECK(expr) do { if (!(expr)) { fprintf(stderr, "line %d: %s\n", __LINE__, #expr); exit(1); } } while (0) +static int failed_stage = -1; +static bool crash_at_stage; +int pure_store_test_fault(PureStoreStage stage) { + if ((int)stage != failed_stage) return 0; + if (crash_at_stage) _exit(77); + return 1; +} +typedef struct { unsigned char *bytes; unsigned releases; } Owner; +static void release(void *raw) { + Owner *owner = raw; + free(owner->bytes); + owner->bytes = NULL; + ++owner->releases; +} + +static void transfers(void) { + Owner owner = {.bytes = malloc(5)}; + CHECK(owner.bytes); + memcpy(owner.bytes, "a\0bcd", 5); + PureTransfer transfer = {0}; + pure_transfer_init(&transfer, (PureSpan){(const unsigned char *)"head", 4}, + (PureSpan){owner.bytes, 5}, &owner, release); + CHECK(!pure_transfer_consume(&transfer, 5)); + CHECK(transfer.active && !owner.releases); + CHECK(pure_transfer_consume(&transfer, 0)); + unsigned char received[9]; + size_t used = 0; + while (transfer.active) { + PureSpan span = pure_transfer_next(&transfer, 2); + CHECK(span.size && span.size <= 2); + memcpy(received + used, span.data, span.size); + used += span.size; + CHECK(pure_transfer_consume(&transfer, span.size)); + CHECK((used == 9) == (owner.releases == 1)); + } + CHECK(used == 9 && !memcmp(received, "heada\0bcd", 9)); + pure_transfer_abort(&transfer); + CHECK(owner.releases == 1 && !pure_transfer_consume(&transfer, 0)); + owner.bytes = malloc(3); + CHECK(owner.bytes); + pure_transfer_init(&transfer, (PureSpan){0}, (PureSpan){owner.bytes, 3}, &owner, release); + CHECK(pure_transfer_consume(&transfer, 1)); + pure_transfer_abort(&transfer); + pure_transfer_abort(&transfer); + CHECK(owner.releases == 2 && !transfer.active); + pure_transfer_init(&transfer, (PureSpan){0}, (PureSpan){0}, &owner, release); + CHECK(owner.releases == 3 && !transfer.active); +} + +static void storage(const char *path) { + CHECK(pure_store_init(path) == PURE_OK); + PureStoredView first, second; + CHECK(pure_store_get(path, "missing", 100, &first) == PURE_NOT_FOUND); + CHECK(!first.mapping && !first.mapped_size); + static const unsigned char original[] = {0, 1, 2, 255, 17}; + static const unsigned char updated[] = {9, 8, 7}; + int64_t revision; + CHECK(pure_store_put(path, "work", 0, (PureSpan){original, sizeof(original)}, &revision) == PURE_OK); + CHECK(revision == 1); + CHECK(pure_store_get(path, "work", sizeof(original) - 1, &first) == PURE_BUDGET); + CHECK(!first.mapping && !first.mapped_size); + CHECK(pure_store_get_range(path, "work", 1, 3, 3, &first) == PURE_OK); + CHECK(first.object_size == sizeof(original) && first.body.size == 3 && !memcmp(first.body.data, original + 1, 3)); + pure_store_release(&first); + CHECK(pure_store_get_range(path, "work", SIZE_MAX, 1, 10, &first) == PURE_LIMIT); + CHECK(pure_store_get_range(path, "work", 1, SIZE_MAX, 10, &first) == PURE_LIMIT); + CHECK(pure_store_get_range(path, "work", 1, 3, 2, &first) == PURE_BUDGET); + CHECK(pure_store_get_range(path, "work", sizeof(original), 0, 0, &first) == PURE_OK); + CHECK(!first.mapped_size && !first.body.size); + pure_store_release(&first); + CHECK(pure_store_get(path, "work", sizeof(original), &first) == PURE_OK); + CHECK(first.body.size == sizeof(original) && first.revision == 1); + CHECK(pure_store_put(path, "work", 1, (PureSpan){updated, sizeof(updated)}, &revision) == PURE_OK); + CHECK(revision == 2); + CHECK(!memcmp(first.body.data, original, sizeof(original))); + CHECK(pure_store_get(path, "work", 100, &second) == PURE_OK); + CHECK(second.revision == 2 && second.body.size == sizeof(updated)); + CHECK(!memcmp(second.body.data, updated, sizeof(updated))); + pure_store_release(&first); + pure_store_release(&first); + pure_store_release(&second); + CHECK(pure_store_put(path, "work", 1, (PureSpan){original, sizeof(original)}, &revision) == PURE_CONFLICT); + CHECK(revision == 2); + + for (failed_stage = PURE_AFTER_HEADER; failed_stage <= PURE_BEFORE_RENAME; ++failed_stage) { + CHECK(pure_store_put(path, "work", 2, (PureSpan){original, sizeof(original)}, &revision) == PURE_STORE_ERROR); + CHECK(pure_store_get(path, "work", 100, &first) == PURE_OK); + CHECK(first.revision == 2 && !memcmp(first.body.data, updated, sizeof(updated))); + pure_store_release(&first); + } + failed_stage = PURE_BEFORE_DIR_SYNC; + CHECK(pure_store_put(path, "work", 2, (PureSpan){original, sizeof(original)}, &revision) == PURE_UNCERTAIN); + CHECK(revision == 3); + failed_stage = -1; + CHECK(pure_store_get(path, "work", 100, &first) == PURE_OK); + CHECK(first.revision == 3 && !memcmp(first.body.data, original, sizeof(original))); + pure_store_release(&first); + CHECK(pure_store_put(path, "work", 2, (PureSpan){updated, sizeof(updated)}, &revision) == PURE_CONFLICT); + + /* A crashed unpublished writer leaves only a private file. A new publisher + * must still use the previous committed version, never an incomplete body. */ + pid_t child = fork(); + CHECK(child >= 0); + if (!child) { + failed_stage = PURE_AFTER_HEADER; + crash_at_stage = true; + pure_store_put(path, "work", 3, (PureSpan){updated, sizeof(updated)}, &revision); + _exit(78); + } + int status; + CHECK(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 77); + CHECK(pure_store_init(path) == PURE_OK); + CHECK(pure_store_get(path, "work", 100, &first) == PURE_OK); + CHECK(first.revision == 3 && !memcmp(first.body.data, original, sizeof(original))); + pure_store_release(&first); + CHECK(pure_store_put(path, "work", 3, (PureSpan){updated, sizeof(updated)}, &revision) == PURE_OK); + CHECK(revision == 4); + + char file[320]; + snprintf(file, sizeof(file), "%s/obj-work", path); + int fd = open(file, O_WRONLY); + CHECK(fd >= 0); + const unsigned char maximum[] = {255,255,255,255,255,255,255,127}; + CHECK(pwrite(fd, maximum, 8, 8) == 8 && close(fd) == 0); + CHECK(pure_store_put(path, "work", INT64_MAX, (PureSpan){original, sizeof(original)}, &revision) == PURE_LIMIT); + CHECK(pure_store_put(path, "empty", 0, (PureSpan){0}, &revision) == PURE_OK); + CHECK(pure_store_get(path, "empty", 0, &first) == PURE_OK && first.body.size == 0); + pure_store_release(&first); + + /* Range mapping crosses page boundaries without copying a whole object. */ + long page = sysconf(_SC_PAGESIZE); + CHECK(page >= 128 && (size_t)page <= PURE_OBJECT_LIMIT / 3); + size_t size = (size_t)page * 3, offset = (size_t)page + 13, length = (size_t)page + 105; + unsigned char *bytes = malloc(size); + CHECK(bytes); + for (size_t i = 0; i < size; ++i) bytes[i] = (unsigned char)((i * 17u) ^ (i >> 8) ^ (i >> 16)); + CHECK(pure_store_put(path, "range", 0, (PureSpan){bytes, size}, &revision) == PURE_OK); + CHECK(pure_store_get_range(path, "range", offset, length, length, &first) == PURE_OK); + CHECK(first.body.size == length && !memcmp(first.body.data, bytes + offset, length)); + pure_store_release(&first); + free(bytes); + snprintf(file, sizeof(file), "%s/obj-broken", path); + fd = open(file, O_CREAT | O_WRONLY | O_EXCL, 0600); + CHECK(fd >= 0 && write(fd, "incomplete", 10) == 10 && close(fd) == 0); + CHECK(pure_store_get(path, "broken", 100, &first) == PURE_STORE_ERROR && !first.mapped_size); + CHECK(pure_store_put(path, "broken", 0, (PureSpan){updated, sizeof(updated)}, &revision) == PURE_STORE_ERROR); + CHECK(pure_store_init(file) == PURE_STORE_ERROR); + struct stat info; + CHECK(stat(file, &info) == 0 && info.st_size == 10); +} + +int main(void) { + transfers(); + char directory[] = "/tmp/pure-connection-unit-XXXXXX"; + CHECK(mkdtemp(directory)); + char path[256]; + snprintf(path, sizeof(path), "%s/store", directory); + storage(path); + DIR *entries = opendir(path); + CHECK(entries); + struct dirent *entry; + while ((entry = readdir(entries))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + CHECK(unlinkat(dirfd(entries), entry->d_name, 0) == 0); + } + CHECK(closedir(entries) == 0 && rmdir(path) == 0); + CHECK(rmdir(directory) == 0); + puts("{\"transfer\":\"partial-consumption-and-single-release\"," + "\"storage\":\"mapped-ranges-snapshot-budget-conflict-failure-uncertain-crash-overflow-empty\"}"); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-program.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-program.c new file mode 100644 index 0000000..6a4a19d --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-program.c @@ -0,0 +1,111 @@ +#include "program.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#define CHECK(expr) do { if (!(expr)) { fprintf(stderr, "line %d: %s\n", __LINE__, #expr); exit(1); } } while (0) +static const char get[] = "on get { let o = store.read(request.key); reply(o, o.body); }"; +static const char put[] = "on put { let s = store.write(request.key, request.revision, request.body); reply(s); }"; +static PureProgram parse(const char *source) { + PureProgram program; + char error[160]; + if (!pure_program_parse(source, strlen(source), &program, error, sizeof(error))) { + fprintf(stderr, "%s\n", error); exit(1); + } + return program; +} +static void invalid(const char *source) { + PureProgram program; + memset(&program, 0x5a, sizeof(program)); + unsigned char original[sizeof(program)]; + memcpy(original, &program, sizeof(program)); + char error[160]; + CHECK(!pure_program_parse(source, strlen(source), &program, error, sizeof(error))); + CHECK(*error && !memcmp(original, &program, sizeof(program))); +} +int main(void) { + char source[4096]; + snprintf(source, sizeof(source), "%s %s", get, put); + PureProgram plan = parse(source); + CHECK(plan.read.to_end && plan.write.to_end && !plan.reply_input); + CHECK(!plan.view_definitions && !plan.read_required && !plan.input_required); + snprintf(source, sizeof(source), "%s on put { let a = range(request.body, 2, 10); " + "let b = range(a, 3, 4); let alias = b; " + "let saved = store.write(request.key, request.revision, alias); reply(saved, b); }", get); + plan = parse(source); + CHECK(plan.write.offset == 5 && plan.write.length == 4 && !plan.write.to_end); + CHECK(plan.reply.offset == 5 && plan.reply.length == 4 && plan.reply_input); + CHECK(plan.input_required == 12 && plan.range_definitions == 2 && plan.view_definitions == 3); + /* Ordered reference views keep even unused bounds failures observable. */ + unsigned char bytes[160]; + for (size_t i = 0; i < sizeof(bytes); ++i) bytes[i] = (unsigned char)(i * 7); + size_t comparisons = 0; + for (size_t a = 0; a < 4; ++a) for (size_t b = 0; b < 6; ++b) + for (size_t c = 0; c <= b; ++c) for (size_t d = 0; d <= b - c; ++d) { + size_t unused_end = (a + b + c) % 12; + snprintf(source, sizeof(source), "on get { let o = store.read(request.key); " + "let unused = range(o.body, %zu, 0); let first = range(o.body, %zu, %zu); " + "let second = range(first, %zu, %zu); reply(o, second); } %s", + unused_end, a, b, c, d, put); + plan = parse(source); + for (size_t n = 0; n < 16; ++n) { + bool reference_ok = unused_end <= n && a <= n && b <= n - a; + PureSpan selected; + bool prepared_ok = n >= plan.read_required && + pure_region_span(plan.read, (PureSpan){bytes, n}, &selected); + CHECK(reference_ok == prepared_ok); + if (reference_ok) { + CHECK(selected.size == d); + CHECK(!d || !memcmp(selected.data, bytes + a + c, d)); + } + ++comparisons; + } + } + snprintf(source, sizeof(source), "on get { let o = store.read(request.key); reply(o); } %s", put); + plan = parse(source); + CHECK(!plan.read.to_end && !plan.read.length); + const char *bad_get[] = { + "on get { let o = store.read(request.key); let x = range(o.body, 0, 2); let y = range(x, 2, 1); reply(o, y); }", + "on get { let o = store.read(request.key); let x = range(o.body, 184467440737095516160, 1); reply(o, x); }", + "on get { let o = store.read(request.key); let x = range(o.body, 1449984, 1); reply(o, x); }", + "on get { let o = store.read(request.key); let o = o.body; reply(o); }", + "on get { let o = store.read(request.key); let x = x; reply(o, x); }", + "on get { let o = store.read(request.key); let x = store.read(request.key); reply(x); }", + "on get { let o = store.read(request.key); reply(o, request.body); }", + "on get { let o = store.read(request.key); let x = o.body; reply(x); }", + "on get { let o = store.read(request.key); reply(o.body); }", + "on get { let o = store.read(request.key); reply(o); let x = o.body; }", + "on get { let o = store.read(request.key); reply(o);", "on get {}" + }; + for (size_t i = 0; i < sizeof(bad_get) / sizeof(*bad_get); ++i) { + snprintf(source, sizeof(source), "%s %s", bad_get[i], put); invalid(source); + } + const char *bad_put[] = { + "on put { let s = store.write(request.key, request.revision, request.body); let b = range(request.body, 0, 1); reply(s, b); }", + "on put { let s = store.write(request.key, request.revision, request.body); reply(s, s.body); }", + "on put { let s = store.read(request.key); reply(s); }" + }; + for (size_t i = 0; i < sizeof(bad_put) / sizeof(*bad_put); ++i) { + snprintf(source, sizeof(source), "%s %s", get, bad_put[i]); invalid(source); + } + invalid(get); + snprintf(source, sizeof(source), "%s %s %s", get, get, put); invalid(source); + snprintf(source, sizeof(source), "%s %s garbage", get, put); invalid(source); + snprintf(source, sizeof(source), "on get { let o = store.read(request.key); "); + for (size_t i = 0; i < 32; ++i) { + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "let x%zu = o.body; ", i); + } + size_t used = strlen(source); + snprintf(source + used, sizeof(source) - used, "reply(o); } %s", put); invalid(source); + char oversized[PURE_PROGRAM_LIMIT + 1]; + memset(oversized, ' ', sizeof(oversized)); + char error[160]; + CHECK(!pure_program_parse(oversized, sizeof(oversized), &plan, error, sizeof(error))); + const char embedded[] = "on get\0{}"; + CHECK(!pure_program_parse(embedded, sizeof(embedded) - 1, &plan, error, sizeof(error))); + PureSpan selected; + CHECK(pure_region_span((PureRegion){0}, (PureSpan){0}, &selected) && !selected.data && !selected.size); + printf("{\"program\":\"prepare-views-preserve-guards-types-effects-and-atomic-install\"," + "\"boundary_semantic_comparisons\":%zu}\n", comparisons); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-runtime.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-runtime.c new file mode 100644 index 0000000..0ff6a8b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-runtime.c @@ -0,0 +1,285 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static const PureProgram identity = { + .read = {.to_end = true}, .write = {.to_end = true}, + .reply = {.to_end = true}, .reply_input = true +}; +typedef struct { + size_t allocations, writes, frees, live; + bool allocation_failure, oversized_read, reenter; + PureRuntime *runtime; + unsigned char received[256]; + size_t received_size; +} Host; +typedef struct { Host *host; unsigned char bytes[]; } Allocation; + +static void free_input(void *pointer, size_t size) { + Allocation *a = pointer; + assert(size > 0 && a->host->live > 0); + ++a->host->frees; + --a->host->live; + free(a); +} + +static bool allocate(void *context, size_t size, PureResource *out) { + Host *host = context; + if (host->allocation_failure) return false; + Allocation *a = malloc(sizeof(*a) + size); + assert(a); + a->host = host; + ++host->allocations; + ++host->live; + *out = (PureResource){.body = {a->bytes, size}, .allocation = a, + .allocation_size = size, .release = free_input}; + return true; +} + +static PureResult write_value(void *context, const char *key, int64_t expected, + PureSpan body, int64_t *revision) { + Host *host = context; + (void)key; + if (host->reenter) { + PureTicket ignored; + assert(pure_runtime_accept(host->runtime, PURE_INPUT_WRITE, "reenter", 0, 0, &ignored) == PURE_INVALID); + assert(!pure_runtime_step(host->runtime)); + } + if (expected == 99) { *revision = 1; return PURE_CONFLICT; } + ++host->writes; + assert(body.size <= sizeof(host->received)); + if (body.size) memcpy(host->received, body.data, body.size); + host->received_size = body.size; + *revision = 1; + return PURE_OK; +} + +static PureResult read_value(void *context, const char *key, PureRegion range, + size_t minimum, size_t available, PureResource *out, int64_t *revision) { + Host *host = context; + (void)key; (void)range; (void)minimum; + if (!host->oversized_read) return PURE_NOT_FOUND; + assert(available < 1000); + assert(allocate(host, available + 1, out)); + *revision = 1; + return PURE_OK; /* Deliberately violates the available-credit contract. */ +} + +static PureHost port(Host *host) { + return (PureHost){.context = host, .allocate = allocate, .read = read_value, .write = write_value}; +} + +static PureTicket submit(PureRuntime *r, const char *key, const void *bytes, size_t length) { + PureTicket ticket; + assert(pure_runtime_accept(r, PURE_INPUT_WRITE, key, 0, length, &ticket) == PURE_OK); + size_t capacity; + unsigned char *input = pure_runtime_ingress(ticket, &capacity); + assert(capacity == length); + if (length) memcpy(input, bytes, length); + assert(pure_runtime_ready(ticket)); + assert(!pure_runtime_ready(ticket)); + return ticket; +} + +static PureOutcome completed(PureRuntime *r, PureTicket ticket) { + PureOutcome result; + while (!pure_runtime_result(ticket, &result)) assert(pure_runtime_step(r)); + return result; +} + +static void admission_and_lifecycle(void) { + Host host = {0}; + PureRuntime r; + PureWork work[3]; + assert(pure_runtime_init(&r, work, 3, &identity, port(&host), 5)); + host.runtime = &r; + PureTicket a, untouched = {0}, out = untouched; + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "../bad", 0, 1, &out) == PURE_INVALID); + assert(!out.runtime && !host.allocations); + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "a", 0, 4, &a) == PURE_OK); + assert(r.retained == 4 && !pure_runtime_step(&r) && !host.writes); + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "b", 0, 2, &out) == PURE_BUDGET); + assert(!out.runtime && host.allocations == 1); + assert(pure_runtime_cancel(a)); + PureOutcome cancelled; + assert(pure_runtime_result(a, &cancelled) && cancelled.status == PURE_CANCELLED); + assert(r.retained == 0 && host.frees == 1 && !pure_runtime_cancel(a)); + assert(pure_runtime_release(a) && !pure_runtime_release(a)); + assert(!pure_runtime_result(a, &cancelled)); + + PureTicket first = submit(&r, "first", "1", 1); + PureTicket middle = submit(&r, "middle", "2", 1); + PureTicket last = submit(&r, "last", "3", 1); + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "full", 0, 0, &out) == PURE_BUSY); + assert(pure_runtime_cancel(middle)); + assert(pure_runtime_release(middle)); + pure_runtime_stop(&r); + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "closed", 0, 0, &out) == PURE_CLOSED); + assert(!pure_runtime_restart(&r, &identity, port(&host), 5)); + host.reenter = true; + assert(completed(&r, first).status == PURE_OK && host.received[0] == '1'); + assert(completed(&r, last).status == PURE_OK && host.received[0] == '3'); + assert(!pure_runtime_step(&r)); + assert(pure_runtime_release(first) && pure_runtime_release(last)); + assert(r.active == 0 && r.retained == 0 && host.live == 0); + assert(pure_runtime_restart(&r, &identity, port(&host), 5)); + PureTicket next = submit(&r, "next", "4", 1); + assert(next.generation > last.generation && !pure_runtime_ready(a)); + assert(!pure_runtime_release(last)); + assert(pure_runtime_release(next)); /* queued input: no effect */ + assert(host.writes == 2 && !pure_runtime_step(&r)); + + host.allocation_failure = true; + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "no_memory", 0, 1, &out) == PURE_ALLOCATION); + assert(r.active == 0 && r.retained == 0); + host.allocation_failure = false; + PureHost denied = port(&host); + denied.write = NULL; + assert(pure_runtime_restart(&r, &identity, denied, 5)); + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "denied", 0, 1, &out) == PURE_DENIED); + assert(pure_runtime_restart(&r, &identity, port(&host), 5)); + host.oversized_read = true; + assert(pure_runtime_accept(&r, PURE_INPUT_READ, "bad_host", 0, 0, &out) == PURE_OK); + assert(completed(&r, out).status == PURE_BUDGET && host.live == 0 && r.retained == 0); + assert(pure_runtime_release(out)); + r.serial = UINT64_MAX; + assert(pure_runtime_accept(&r, PURE_INPUT_WRITE, "exhausted", 0, 0, &out) == PURE_LIMIT); +} + +static void forwarding(void) { + Host a = {0}, b = {0}, c = {0}; + PureRuntime source, left, right; + PureWork source_work[1], left_work[1], right_work[1]; + assert(pure_runtime_init(&source, source_work, 1, &identity, port(&a), 8)); + assert(pure_runtime_init(&left, left_work, 1, &identity, port(&b), 2)); + assert(pure_runtime_init(&right, right_work, 1, &identity, port(&c), 8)); + PureTicket original = submit(&source, "original", "value", 5), x = {0}, y; + PureOutcome result = completed(&source, original); + assert(result.status == PURE_OK); + const unsigned char *backing = result.body.data; + assert(pure_runtime_forward(original, &left, "left", 0, &x) == PURE_BUDGET); + assert(!x.runtime && source.work[0].pins == 0); + assert(pure_runtime_restart(&left, &identity, port(&b), 8)); + assert(pure_runtime_forward(original, &left, "left", 0, &x) == PURE_OK); + assert(pure_runtime_forward(original, &right, "right", 0, &y) == PURE_OK); + assert(pure_runtime_release(original)); + assert(!pure_runtime_result(original, &result)); + pure_runtime_stop(&source); + assert(!pure_runtime_restart(&source, &identity, port(&a), 8)); + assert(a.live == 1 && !b.allocations && !c.allocations); + assert(pure_runtime_cancel(x) && pure_runtime_release(x)); + assert(a.live == 1 && !b.writes); + result = completed(&right, y); + assert(result.body.data == backing && result.body.size == 5 && !memcmp(result.body.data, "value", 5)); + assert(c.writes == 1 && !c.allocations && source.retained == 5); + assert(!pure_runtime_cancel(y)); /* effects cannot be rolled back */ + assert(pure_runtime_release(y)); + assert(a.frees == 1 && !source.active && !source.retained && !right.active && !right.retained); + assert(pure_runtime_restart(&source, &identity, port(&a), 8)); + + /* A completed consumer does not retain its input when the program has no + * input-backed reply. The producing domain can then be reclaimed early. */ + PureProgram no_reply = identity; + no_reply.reply_input = false; + assert(pure_runtime_restart(&right, &no_reply, port(&c), 8)); + original = submit(&source, "again", "last", 4); + assert(completed(&source, original).status == PURE_OK); + assert(pure_runtime_forward(original, &right, "no_reply", 0, &y) == PURE_OK); + assert(pure_runtime_release(original)); + assert(completed(&right, y).body.size == 0 && a.live == 0); + assert(source.active == 0 && right.active == 1 && right.retained == 0); + assert(pure_runtime_release(y)); +} + +static void long_chain(void) { + enum { COUNT = 1024 }; + PureWork *work = calloc(COUNT, sizeof(*work)); + assert(work); + Host host = {0}; + PureRuntime r; + assert(pure_runtime_init(&r, work, COUNT, &identity, port(&host), COUNT)); + PureTicket ticket = submit(&r, "chain", "x", 1); + for (size_t i = 1; i < COUNT; ++i) { + assert(completed(&r, ticket).status == PURE_OK); + PureTicket next; + assert(pure_runtime_forward(ticket, &r, "chain", 0, &next) == PURE_OK); + assert(pure_runtime_release(ticket)); + ticket = next; + } + assert(completed(&r, ticket).status == PURE_OK); + assert(host.allocations == 1 && r.active == COUNT); + assert(pure_runtime_release(ticket)); + assert(r.retained == 0 && r.active == 0 && host.frees == 1); + free(work); +} + +static void actual_storage(void) { + char directory[] = "/tmp/pure-runtime-check-XXXXXX"; + assert(mkdtemp(directory)); + char source_path[128], target_path[128], name[160]; + snprintf(source_path, sizeof(source_path), "%s/source", directory); + snprintf(target_path, sizeof(target_path), "%s/target", directory); + assert(pure_store_init(source_path) == PURE_OK && pure_store_init(target_path) == PURE_OK); + PureProgram selection; + char error[160]; + assert(pure_program_load("examples/selection.pure", &selection, error, sizeof(error))); + PureRuntime source, target; + PureWork a[2], b[2]; + assert(pure_runtime_init(&source, a, 2, &selection, pure_store_host(source_path), 128)); + assert(pure_runtime_init(&target, b, 2, &identity, pure_store_host(target_path), 128)); + const unsigned char bytes[] = {1,0,0,0, 3,0,0,0, 'a','b','c','d','e'}; + PureTicket ticket = submit(&source, "excerpt", bytes, sizeof(bytes)), consumer; + PureOutcome result = completed(&source, ticket); + assert(result.status == PURE_OK && result.body.size == 3 && !memcmp(result.body.data, "bcd", 3)); + assert(pure_runtime_forward(ticket, &target, "archive", 0, &consumer) == PURE_OK); + assert(pure_runtime_release(ticket)); + assert(source.active == 1 && source.retained == sizeof(bytes)); + assert(completed(&target, consumer).status == PURE_OK); + assert(pure_runtime_release(consumer) && source.active == 0); + PureStoredView stored = {0}; + assert(pure_store_get(target_path, "archive", 128, &stored) == PURE_OK); + assert(stored.body.size == 3 && !memcmp(stored.body.data, "bcd", 3)); + pure_store_release(&stored); + + assert(pure_runtime_accept(&source, PURE_INPUT_READ, "excerpt", 0, 0, &ticket) == PURE_OK); + assert(completed(&source, ticket).status == PURE_OK); + assert(pure_runtime_forward(ticket, &target, "snapshot", 0, &consumer) == PURE_OK); + assert(pure_runtime_release(ticket)); + int64_t revision; + assert(pure_store_put(source_path, "excerpt", 1, (PureSpan){(const unsigned char *)"new", 3}, &revision) == PURE_OK); + result = completed(&target, consumer); + assert(result.body.size == 3 && !memcmp(result.body.data, "bcd", 3)); + assert(pure_runtime_release(consumer)); + + unsigned char invalid[] = {255,255,255,255, 1,0,0,0, 'x'}; + ticket = submit(&source, "invalid", invalid, sizeof(invalid)); + assert(completed(&source, ticket).status == PURE_LIMIT); + assert(pure_runtime_release(ticket)); + assert(pure_store_get(source_path, "invalid", 128, &stored) == PURE_NOT_FOUND); + assert(source.active == 0 && target.active == 0 && source.retained == 0 && target.retained == 0); + const char *files[] = {"source/obj-excerpt", "source/.writer.lock", "target/obj-archive", + "target/obj-snapshot", "target/.writer.lock"}; + for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) { + snprintf(name, sizeof(name), "%s/%s", directory, files[i]); + assert(unlink(name) == 0); + } + assert(rmdir(source_path) == 0 && rmdir(target_path) == 0 && rmdir(directory) == 0); +} + +int main(void) { + admission_and_lifecycle(); + forwarding(); + long_chain(); + actual_storage(); + printf("{\"runtime_tests\":\"passed\",\"domain_bytes\":%zu,\"work_slot_bytes\":%zu," + "\"ticket_bytes\":%zu,\"prepared_program_bytes\":%zu,\"forwarding_chain\":1024," + "\"native_storage_pipeline\":true}\n", sizeof(PureRuntime), sizeof(PureWork), + sizeof(PureTicket), sizeof(PureProgram)); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-splice.c b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-splice.c new file mode 100644 index 0000000..5b8232b --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/connections/test-splice.c @@ -0,0 +1,172 @@ +#define _POSIX_C_SOURCE 200809L +#include "runtime.h" +#include "store-port.h" +#include "store.h" +#include <dirent.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0) +static int failed_stage = -1; +int pure_store_test_fault(PureStoreStage stage) { return (int)stage == failed_stage; } + +static void expect(const char *path, const char *key, int64_t revision, PureSpan bytes) { + PureStoredView view; + CHECK(pure_store_get(path, key, PURE_OBJECT_LIMIT, &view) == PURE_OK); + CHECK(view.revision == revision && view.body.size == bytes.size); + CHECK(!bytes.size || !memcmp(view.body.data, bytes.data, bytes.size)); + pure_store_release(&view); +} + +static void parser(void) { + const char *get = "on get { let o = store.read(request.key); reply(o, o.body); }"; + const char *layout = "layout E { marker: bytes[1]; remove: u32le; start: u32le; insert: bytes; }"; + char source[2048], error[160]; + PureProgram p; + snprintf(source, sizeof(source), "%s %s on put { let e = view(E, request.body); " + "let s = store.splice(request.key, request.revision, e.start, e.remove, e.insert); reply(s, e.insert); }", layout, get); + CHECK(pure_program_parse(source, strlen(source), &p, error, sizeof(error))); + CHECK(p.splice && p.input_required == 9 && p.splice_offset.value == 5 && p.splice_remove.value == 1); + unsigned char input[] = {0, 2,0,0,0, 3,0,0,0, 0,255}; + PureSpan write, reply; + size_t offset, remove; + for (size_t n = 0; n < 9; ++n) + CHECK(!pure_program_input(&p, (PureSpan){input, n}, &write, &reply)); + CHECK(pure_program_input(&p, (PureSpan){input, sizeof(input)}, &write, &reply)); + CHECK(write.size == 2 && write.data == input + 9 && reply.data == write.data); + CHECK(pure_program_splice(&p, (PureSpan){input, sizeof(input)}, &offset, &remove)); + CHECK(offset == 3 && remove == 2); + + snprintf(source, sizeof(source), "%s on put { let s = store.splice(request.key, request.revision, 0, 0, request.body); reply(s); }", get); + CHECK(pure_program_parse(source, strlen(source), &p, error, sizeof(error))); + CHECK(pure_program_splice(&p, (PureSpan){0}, &offset, &remove) && offset == 0 && remove == 0); + const char *invalid[] = { + "let s = store.splice(request.key, request.revision, e.insert, 0, e.insert); reply(s);", + "let s = store.splice(request.key, request.revision, 0, 1449985, e.insert); reply(s);", + "let s = store.splice(request.key, request.revision, 0, 0, e.start); reply(s);", + "let s = store.splice(request.key, request.revision, 0, 0, e.insert); let t = store.write(request.key, request.revision, e.insert); reply(t);" + }; + for (size_t i = 0; i < sizeof(invalid) / sizeof(*invalid); ++i) { + snprintf(source, sizeof(source), "%s %s on put { let e = view(E, request.body); %s }", layout, get, invalid[i]); + unsigned char before[sizeof(p)]; + memcpy(before, &p, sizeof(p)); + CHECK(!pure_program_parse(source, strlen(source), &p, error, sizeof(error))); + CHECK(*error && !memcmp(before, &p, sizeof(p))); + } +} + +static void storage(const char *path) { + /* Reference whole-value assembly exists only in this test oracle. */ + unsigned char oracle[65536], before[65536], insert[9000]; + for (size_t i = 0; i < sizeof(insert); ++i) insert[i] = (unsigned char)(i * 19u); + size_t length = 49157; + for (size_t i = 0; i < length; ++i) oracle[i] = (unsigned char)((i * 17u) ^ (i >> 8)); + int64_t revision; + CHECK(pure_store_splice(path, "work", 0, 0, 0, (PureSpan){oracle, length}, &revision) == PURE_OK); + CHECK(revision == 1); + /* Prefix/suffix copies cross both scratch and mapping page boundaries. */ + const size_t edits[][3] = {{16381, 7, 9000}, {0, 0, 5}, {50000, 1, 0}, {4095, 4097, 3}, {0, 0, 0}}; + for (size_t i = 0; i < sizeof(edits) / sizeof(*edits); ++i) { + size_t offset = edits[i][0], remove = edits[i][1], count = edits[i][2], previous = length; + memcpy(before, oracle, length); + PureStoredView old; + CHECK(pure_store_get(path, "work", PURE_OBJECT_LIMIT, &old) == PURE_OK); + int64_t expected = revision; + CHECK(pure_store_splice(path, "work", expected, offset, remove, (PureSpan){insert, count}, &revision) == PURE_OK); + CHECK(revision == expected + 1); + memmove(oracle + offset + count, oracle + offset + remove, length - offset - remove); + memcpy(oracle + offset, insert, count); + length = length - remove + count; + expect(path, "work", revision, (PureSpan){oracle, length}); + CHECK(old.body.size == previous && !memcmp(old.body.data, before, previous)); + pure_store_release(&old); + } + int64_t latest = revision; + CHECK(pure_store_splice(path, "work", latest - 1, 0, 0, (PureSpan){insert, 1}, &revision) == PURE_CONFLICT); + CHECK(revision == latest); + CHECK(pure_store_splice(path, "work", latest, SIZE_MAX, 0, (PureSpan){0}, &revision) == PURE_LIMIT); + CHECK(pure_store_splice(path, "work", latest, 0, SIZE_MAX, (PureSpan){0}, &revision) == PURE_LIMIT); + CHECK(pure_store_splice(path, "work", latest, length + 1, 0, (PureSpan){0}, &revision) == PURE_LIMIT); + /* Rejected before reading the intentionally undersized insertion pointer. */ + CHECK(pure_store_splice(path, "work", latest, 0, 0, (PureSpan){insert, PURE_OBJECT_LIMIT}, &revision) == PURE_LIMIT); + expect(path, "work", latest, (PureSpan){oracle, length}); + CHECK(pure_store_splice(path, "missing", 0, 1, 0, (PureSpan){0}, &revision) == PURE_LIMIT); + PureStoredView view; + CHECK(pure_store_get(path, "missing", 0, &view) == PURE_NOT_FOUND); + for (failed_stage = PURE_AFTER_HEADER; failed_stage <= PURE_BEFORE_RENAME; ++failed_stage) { + CHECK(pure_store_splice(path, "work", latest, 1, 3, (PureSpan){insert, 5}, &revision) == PURE_STORE_ERROR); + expect(path, "work", latest, (PureSpan){oracle, length}); + } + failed_stage = PURE_BEFORE_DIR_SYNC; + CHECK(pure_store_splice(path, "work", latest, length, 0, (PureSpan){insert, 5}, &revision) == PURE_UNCERTAIN); + CHECK(revision == latest + 1); + failed_stage = -1; + memcpy(oracle + length, insert, 5); length += 5; + expect(path, "work", revision, (PureSpan){oracle, length}); + CHECK(pure_store_splice(path, "work", revision, 0, length, (PureSpan){0}, &revision) == PURE_OK); + expect(path, "work", revision, (PureSpan){0}); + char file[256]; + snprintf(file, sizeof(file), "%s/obj-work", path); + int fd = open(file, O_WRONLY); + const unsigned char maximum[] = {255,255,255,255,255,255,255,127}; + CHECK(fd >= 0 && pwrite(fd, maximum, 8, 8) == 8 && close(fd) == 0); + CHECK(pure_store_splice(path, "work", INT64_MAX, 0, 0, (PureSpan){0}, &revision) == PURE_LIMIT); +} + +static void runtime(const char *path) { + PureProgram program; + char error[160]; + CHECK(pure_program_load("examples/editor.pure", &program, error, sizeof(error))); + PureRuntime runtime; + PureWork slots[1]; + PureHost denied = pure_store_host(path); + denied.splice = NULL; /* ordinary writes do not grant permission to splice */ + CHECK(pure_runtime_init(&runtime, slots, 1, &program, denied, 32)); + PureTicket ticket = {0}; + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_WRITE, "native", 0, 9, &ticket) == PURE_DENIED); + CHECK(!ticket.runtime && runtime.active == 0 && runtime.retained == 0); + CHECK(pure_runtime_restart(&runtime, &program, pure_store_host(path), 32)); + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_WRITE, "native", 0, 9, &ticket) == PURE_OK); + CHECK(pure_runtime_cancel(ticket) && pure_runtime_release(ticket)); + CHECK(!pure_runtime_step(&runtime)); + PureStoredView view; + CHECK(pure_store_get(path, "native", 32, &view) == PURE_NOT_FOUND); + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_WRITE, "native", 0, 7, &ticket) == PURE_OK); + memset(pure_runtime_ingress(ticket, NULL), 0, 7); + CHECK(pure_runtime_ready(ticket) && pure_runtime_step(&runtime)); + PureOutcome result; + CHECK(pure_runtime_result(ticket, &result) && result.status == PURE_LIMIT); + CHECK(pure_runtime_release(ticket) && runtime.commits == 0); + CHECK(pure_runtime_accept(&runtime, PURE_INPUT_WRITE, "native", 0, 9, &ticket) == PURE_OK); + unsigned char *input = pure_runtime_ingress(ticket, NULL); + memset(input, 0, 8); input[8] = 'x'; + CHECK(pure_runtime_ready(ticket) && pure_runtime_step(&runtime)); + CHECK(pure_runtime_result(ticket, &result) && result.status == PURE_OK && result.revision == 1); + CHECK(result.body.size == 1 && result.body.data == input + 8 && runtime.retained == 9); + expect(path, "native", 1, (PureSpan){(const unsigned char *)"x", 1}); + CHECK(pure_runtime_release(ticket) && runtime.retained == 0 && runtime.active == 0); +} + +int main(void) { + parser(); + char path[] = "/tmp/pure-splice-check-XXXXXX"; + CHECK(mkdtemp(path) && pure_store_init(path) == PURE_OK); + storage(path); + runtime(path); + DIR *entries = opendir(path); + CHECK(entries); + struct dirent *entry; + while ((entry = readdir(entries))) { + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; + CHECK(strncmp(entry->d_name, ".pending-", 9)); + CHECK(unlinkat(dirfd(entries), entry->d_name, 0) == 0); + } + CHECK(closedir(entries) == 0 && rmdir(path) == 0); + puts("{\"splice_tests\":\"passed\",\"checks\":[\"authored-fields-and-atomic-parse-failure\"," + "\"binary-edit-oracle-and-old-snapshot\",\"bounds-conflict-size-overflow\"," + "\"publication-failures-and-uncertain-durability\",\"cancel-admission-and-borrowed-reply\"]}"); + return 0; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/cli.mjs b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/cli.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { createInterface } from 'node:readline'; +import { createRuntime } from './web/runtime.mjs'; +import { parseProject } from './web/project.mjs'; +import { remoteTarget, pushProject, pullProject } from './remote.mjs'; + +const help = `Pure authoring workbench · 같은 정의와 Zig 실행기 + load PATH .pure 파일을 적용 (같은 이름의 상태 보존) + down NAME 정의한 입력 누르기 + up NAME 정의한 입력 놓기 + point NAME PHASE X Y 좌표 입력 (down / move / up / cancel) + point-target NAME VIEW PHASE X Y 그룹의 대상에 좌표 입력 + change NAME VALUE 숫자 입력 전달 + frame [1..600] 시간 진행, 1프레임 = 1/60초 + show 상태와 도형 출력 + limits C H W 표시 캐시(0..32)·변경 기록(0..8)·작업량(0..4294967295) 설정 + forget [PART] 표시 기록 삭제: all, values, history (상태는 유지) + costs 마지막 표시 계산의 작업량·명령 수·캐시 적중 출력 + reset 현재 정의의 초기 상태로 + save PATH 적용한 정의를 저장 + save-project PATH 적용 코드와 현재 배치를 JSON으로 저장 + restore PATH 저장한 배치를 복원 (초안이 있으면 파일에 보존) + connect URL KEY 로컬 네이티브 저장 서버의 작업물에 연결 + push [REVISION] 현재 배치를 저장 (첫 생성은 push 0, 충돌 시 기존 값 유지) + pull 원격 배치를 검증·복원하고 revision 기억 + help / quit +브라우저와 별도 인스턴스입니다. 임의 JavaScript는 실행하지 않습니다.`; + +async function main() { + const args = process.argv.slice(2); + if (args.includes('--help')) { console.log(help); return; } + if (args.length > 1) throw new Error('사용법: node cli.mjs [project.pure]'); + const runtime = await createRuntime(await readFile(new URL('./web/core.wasm', import.meta.url))); + let savedDraft = null; + let remote = null, remoteRevision = null; + async function loadFile(path, restoreOnly = false) { + const content = await readFile(path, 'utf8'); + if (restoreOnly || path.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + savedDraft = runtime.restoreDocument(project).draft; + return; + } + if (restoreOnly) throw new Error('현재 배치가 포함된 version 2 파일이 필요합니다.'); + runtime.apply(project.source); + } else runtime.apply(content); + savedDraft = null; + } + if (args[0]) await loadFile(args[0]); else runtime.apply(''); + const terminal = createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) }); + if (process.stdin.isTTY) console.log(help); + terminal.setPrompt('pure> '); + if (process.stdin.isTTY) terminal.prompt(); + for await (const line of terminal) { + try { + const match = /^(\S+)(?:\s+(.+))?$/.exec(line.trim()); + const name = match?.[1] || 'show'; + const arg = match?.[2]; + if (name === 'quit') { terminal.close(); break; } + if (name === 'help') console.log(help); + else if (name === 'costs' && !arg) console.log(JSON.stringify({ evaluation: runtime.evaluationStats() })); + else { + if (name === 'load' && arg) await loadFile(arg); + else if (name === 'restore' && arg) await loadFile(arg, true); + else if (name === 'connect' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 2) throw new Error('connect URL KEY 형식을 사용하세요.'); + remote = remoteTarget(parts[0], parts[1]); + remoteRevision = null; + console.log(JSON.stringify({ remote: remote.href })); + } else if (name === 'push') { + if (!remote) throw new Error('먼저 connect URL KEY로 연결하세요.'); + const expected = arg ?? remoteRevision; + if (expected === null) throw new Error('먼저 pull로 버전을 읽거나 push 0으로 새 작업물을 만드세요.'); + remoteRevision = await pushProject(remote, expected, runtime.exportDocument(savedDraft ?? undefined)); + console.log(JSON.stringify({ remote: remote.href, revision: remoteRevision, stored: true })); + } else if (name === 'pull' && !arg) { + if (!remote) throw new Error('먼저 connect URL KEY로 연결하세요.'); + const loaded = await pullProject(remote); + savedDraft = runtime.restoreDocument(loaded.project).draft; + remoteRevision = loaded.revision; + console.log(JSON.stringify({ remote: remote.href, revision: remoteRevision, restored: true })); + } + else if (name === 'save-project' && arg) await writeFile(arg, JSON.stringify(runtime.exportDocument(savedDraft ?? undefined), null, 2) + '\n', {flag:'wx'}); + else if (name === 'save' && arg) await writeFile(arg, runtime.exportProject().source, { flag: 'wx' }); + else if (name === 'down' && arg) runtime.down(arg, 'terminal'); + else if (name === 'up' && arg) runtime.up(arg, 'terminal'); + else if (name === 'point' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 4) throw new Error('point NAME PHASE X Y 형식을 사용하세요.'); + runtime.point(parts[0], parts[1], Number(parts[2]), Number(parts[3]), 'terminal'); + } else if (name === 'point-target' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 5) throw new Error('point-target NAME VIEW PHASE X Y 형식을 사용하세요.'); + runtime.point(parts[0], parts[2], Number(parts[3]), Number(parts[4]), 'terminal', parts[1]); + } else if (name === 'change' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 2) throw new Error('change NAME VALUE 형식을 사용하세요.'); + runtime.change(parts[0], Number(parts[1]), 'terminal'); + } else if (name === 'frame') { + if (arg !== undefined && !/^\d+$/.test(arg)) throw new Error('frame 1..600 정수를 입력하세요.'); + const count = arg === undefined ? 1 : Number(arg); + if (count < 1 || count > 600) throw new Error('frame 1..600 정수를 입력하세요.'); + runtime.frame(count); + } else if (name === 'reset' && !arg) runtime.reset(); + else if (name === 'limits' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 3 || parts.some(part => !/^\d+$/.test(part))) + throw new Error('limits C H W 정수 세 개를 입력하세요.'); + const [cacheEntries, historyEntries, workLimit] = parts.map(Number); + runtime.configureEvaluation({ cacheEntries, historyEntries, workLimit }); + } else if (name === 'forget') runtime.forgetEvaluation(arg || 'all'); + else if (name !== 'show' || arg) throw new Error('명령 형식을 확인하세요. help로 안내를 볼 수 있습니다.'); + const result = runtime.snapshot(); + console.log(JSON.stringify(result)); + if (result.lastError && !process.stdin.isTTY) process.exitCode = 1; + } + } catch (error) { + console.error(`${error.line ? `${error.line}:${error.column || 1} ` : ''}${error.message}`); + if (!process.stdin.isTTY) process.exitCode = 1; + } + if (process.stdin.isTTY) terminal.prompt(); + } +} + +main().catch((error) => { console.error(error.message); process.exitCode = 1; }); diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/examples/direct.pure b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/examples/direct.pure new file mode 100644 index 0000000..b29721c --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/examples/direct.pure @@ -0,0 +1,47 @@ +# 도형을 잡은 위치를 유지해 이동합니다. 취소하면 잡기 전 위치로 돌아갑니다. +state x = 80 +state y = 160 +state grab_x = 0 +state grab_y = 0 +state start_x = 80 +state start_y = 160 +state dragging = 0 + +input drag = pointer(tile) +input left = ArrowLeft +input right = ArrowRight + +box left_area(20, 100, 280, 220, "#233a31") +box right_area(340, 100, 280, 220, "#343b25") +box tile(x, y, 56, 44, "#c9f08b") +text horizontal(30, 45, x, "x: ") +text vertical(180, 45, y, "y: ") +text region(350, 70, x >= 320, "오른쪽 영역: ") +text active(30, 75, dragging, "잡고 있음: ") + +on drag.down { + start_x = x + start_y = y + grab_x = event_x - x + grab_y = event_y - y + dragging = 1 +} +on drag.move { + x = clamp(event_x - grab_x, 20, 564) + y = clamp(event_y - grab_y, 100, 276) +} +on drag.up { + # 놓기는 마지막으로 표시한 위치를 확정합니다. + dragging = 0 +} +on drag.cancel { + x = start_x + y = start_y + dragging = 0 +} +on left.down { + if dragging == 0 { x = max(20, x - 10) } +} +on right.down { + if dragging == 0 { x = min(564, x + 10) } +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/remote.mjs b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/remote.mjs new file mode 100644 index 0000000..6ee6a59 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/remote.mjs @@ -0,0 +1,89 @@ +import http from 'node:http'; +import { MAX_PROJECT_BYTES, parseProject } from './web/project.mjs'; + +const maxRevision = 9223372036854775807n; +function revisionText(value, allowZero = false) { + if (typeof value !== 'string' || !/^(0|[1-9]\d*)$/.test(value) || + value.length > 19 || BigInt(value) > maxRevision || (!allowZero && value === '0')) + throw new Error('revision은 범위 안의 정수 문자열이어야 합니다.'); + return value; +} + +export function remoteTarget(base, key) { + const url = new URL(base); + if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost'].includes(url.hostname) || + url.username || url.password || url.pathname !== '/' || url.search || url.hash) + throw new Error('현재 연결 호스트는 로컬 http://127.0.0.1:PORT 형식을 사용합니다.'); + if (!/^[A-Za-z0-9_-]{1,64}$/.test(key)) throw new Error('작업물 이름은 영문·숫자·_·- 1~64자입니다.'); + url.hostname = '127.0.0.1'; + url.pathname = `/objects/${key}`; + return url; +} + +function request(url, method, body, headers = {}) { + return new Promise((resolve, reject) => { + let done = false; + const finish = (error, value) => { + if (done) return; + done = true; + clearTimeout(deadline); + if (error) reject(error); else resolve(value); + }; + const req = http.request(url, { method, agent: false, headers: { ...headers, + 'Content-Length': body?.byteLength ?? 0, Connection: 'close' } }, (res) => { + const chunks = []; + let size = 0; + res.on('data', (chunk) => { + size += chunk.length; + if (size > MAX_PROJECT_BYTES) { + const error = new Error('서버 작업물이 허용 크기를 넘었습니다.'); + finish(error); + res.destroy(error); + req.destroy(error); + } else chunks.push(chunk); + }); + res.on('error', (error) => finish(error)); + res.on('aborted', () => finish(new Error('응답이 완료되기 전에 연결이 끊겼습니다.'))); + res.on('end', () => { + if (res.statusCode !== 200) { + const error = new Error(`원격 작업 실패: ${res.statusCode} ${res.headers['x-pure-error'] ?? ''}`.trim()); + error.status = res.statusCode; + error.etag = res.headers.etag; + finish(error); + return; + } + try { + const match = /^"([1-9]\d*)"$/.exec(res.headers.etag ?? ''); + if (!match) throw new Error('서버의 revision 응답이 올바르지 않습니다.'); + const revision = revisionText(match[1]); + finish(null, { revision, bytes: Buffer.concat(chunks, size) }); + } catch (error) { finish(error); } + }); + }); + const deadline = setTimeout(() => { + const error = new Error('원격 작업 시간 초과: 저장 완료 여부는 다시 확인해야 합니다.'); + finish(error); + req.destroy(error); + }, 12000); + req.on('error', (error) => finish(error)); + req.end(body); + }); +} + +export async function pushProject(target, expected, project) { + revisionText(expected, true); + const text = JSON.stringify(project); + parseProject(text); + const body = Buffer.from(text, 'utf8'); + const headers = expected === '0' ? { 'If-None-Match': '*' } : { 'If-Match': `"${expected}"` }; + const result = await request(target, 'PUT', body, headers); + return result.revision; +} + +export async function pullProject(target) { + const result = await request(target, 'GET'); + const text = new TextDecoder('utf-8', { fatal: true }).decode(result.bytes); + const project = parseProject(text); + if (project.version !== 2) throw new Error('현재 배치를 포함한 version 2 작업물이 필요합니다.'); + return { project, revision: result.revision }; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/compiler.mjs b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/compiler.mjs new file mode 100644 index 0000000..6aec335 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/compiler.mjs @@ -0,0 +1,496 @@ +// Pure's experimental scalar interaction language. No host code is evaluated. +const LIMITS = Object.freeze({ sourceBytes: 32768, states: 32, inputs: 8, + views: 24, handlers: 64, instructions: 512, depth: 64, stack: 64 }); +const RESERVED = new Set(['frame', 'dt', 'event_x', 'event_y', 'event_value', 'min', 'max', 'clamp']); +const BINARY = new Map([ + ['||', [1, 15]], ['&&', [2, 14]], ['==', [3, 12]], ['!=', [3, 13]], + ['<', [4, 8]], ['<=', [4, 9]], ['>', [4, 10]], ['>=', [4, 11]], + ['+', [5, 3]], ['-', [5, 4]], ['*', [6, 5]], ['/', [6, 6]], +]); +const FUNCTIONS = { min: [2, 17], max: [2, 18], clamp: [3, 19] }; +const VIEW_FIELDS = { box: ['x', 'y', 'width', 'height'], + circle: ['x', 'y', 'radius'], text: ['x', 'y', 'value'] }; + +function fail(token, message) { + const error = new Error(`${message} (${token.line}:${token.column})`); + error.line = token.line; + error.column = token.column; + throw error; +} + +function lex(source) { + const tokens = []; + let offset = 0, line = 1, column = 1; + const advance = () => { + const char = source[offset++]; + if (char === '\n') { line++; column = 1; } else column++; + return char; + }; + while (offset < source.length) { + const char = source[offset]; + if (char === ' ' || char === '\t' || char === '\r') { advance(); continue; } + if (char === '#') { while (offset < source.length && source[offset] !== '\n') advance(); continue; } + const token = { line, column }; + if (char === '\n') { advance(); tokens.push({ ...token, kind: 'nl' }); continue; } + if (char === '"') { + advance(); + let value = '', closed = false; + while (offset < source.length) { + const next = advance(); + if (next === '"') { closed = true; break; } + if (next === '\n' || next === '\r') fail(token, 'Strings must fit on one line'); + if (next === '\\') { + const escaped = advance(); + const escapes = { n: '\n', r: '\r', t: '\t', '"': '"', '\\': '\\' }; + if (!Object.hasOwn(escapes, escaped)) fail(token, 'Unsupported string escape'); + value += escapes[escaped]; + } else value += next; + } + if (!closed) fail(token, 'Unterminated string'); + tokens.push({ ...token, kind: 'string', value }); continue; + } + const remaining = source.slice(offset); + const number = remaining.match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/); + if (number) { + const value = Number(number[0]); + if (!Number.isFinite(value)) fail(token, 'Number must be finite'); + for (let i = 0; i < number[0].length; i++) advance(); + tokens.push({ ...token, kind: 'number', value }); continue; + } + const name = remaining.match(/^[A-Za-z_][A-Za-z_0-9]*/); + if (name) { + for (let i = 0; i < name[0].length; i++) advance(); + tokens.push({ ...token, kind: 'name', value: name[0] }); continue; + } + const pair = source.slice(offset, offset + 2); + if (['<=', '>=', '==', '!=', '&&', '||'].includes(pair)) { + advance(); advance(); tokens.push({ ...token, kind: pair }); continue; + } + if ('=+-*/!<>(){},.;:'.includes(char)) { + advance(); tokens.push({ ...token, kind: char }); continue; + } + fail(token, `Unexpected character ${JSON.stringify(char)}`); + } + tokens.push({ kind: 'eof', line, column }); + return tokens; +} + +class Parser { + constructor(tokens) { this.tokens = tokens; this.position = 0; this.depth = 0; } + peek() { return this.tokens[this.position]; } + take() { return this.tokens[this.position++]; } + is(kind, value) { return this.peek().kind === kind && (value === undefined || this.peek().value === value); } + expect(kind, description = kind) { + if (!this.is(kind)) fail(this.peek(), `Expected ${description}`); + return this.take(); + } + newlines() { while (this.is('nl')) this.take(); } + separators() { while (this.is('nl') || this.is(';')) this.take(); } + end() { + if (!['nl', ';', '}', 'eof'].includes(this.peek().kind)) fail(this.peek(), 'Expected newline or semicolon'); + this.separators(); + } + reference(token) { + if (!this.is('.')) return token.value; + this.take(); + return `${token.value}.${this.expect('name', 'field name').value}`; + } + initial() { + let sign = 1; + if (this.is('-') || this.is('+')) sign = this.take().kind === '-' ? -1 : 1; + return sign * this.expect('number', 'numeric state initializer').value; + } + node(token, properties, children = []) { + const depth = 1 + Math.max(0, ...children.map(child => child.depth)); + if (depth > LIMITS.depth) fail(token, 'Expression nesting exceeds 64'); + return { token, depth, ...properties }; + } + expression(minimum = 0, multiline = false) { + if (++this.depth > LIMITS.depth) fail(this.peek(), 'Expression nesting exceeds 64'); + if (multiline) this.newlines(); + const token = this.take(); + let left; + if (token.kind === 'number') left = this.node(token, { kind: 'constant', value: token.value }); + else if (token.kind === '-' || token.kind === '!') { + const child = this.expression(7, multiline); + left = this.node(token, { kind: 'unary', op: token.kind === '-' ? 7 : 16, child }, [child]); + } else if (token.kind === '(') { + left = this.expression(0, true); this.newlines(); this.expect(')'); + } else if (token.kind === 'name') { + if (Object.hasOwn(FUNCTIONS, token.value) && this.is('(')) { + this.take(); + const args = []; + this.newlines(); + if (!this.is(')')) { + for (;;) { + args.push(this.expression(0, true)); this.newlines(); + if (!this.is(',')) break; + this.take(); + } + } + this.expect(')'); + if (args.length !== FUNCTIONS[token.value][0]) fail(token, `${token.value} expects ${FUNCTIONS[token.value][0]} arguments`); + left = this.node(token, { kind: 'call', op: FUNCTIONS[token.value][1], args }, args); + } else left = this.node(token, { kind: 'reference', name: this.reference(token) }); + } else fail(token, 'Expected a number, state, or expression'); + for (;;) { + if (multiline) this.newlines(); + const entry = BINARY.get(this.peek().kind); + if (!entry || entry[0] < minimum) break; + const operator = this.take(); + const right = this.expression(entry[0] + 1, multiline); + left = this.node(operator, { kind: 'binary', op: entry[1], left, right }, [left, right]); + } + this.depth--; + return left; + } + block(depth = 0) { + if (depth >= LIMITS.depth) fail(this.peek(), 'Handler nesting exceeds 64'); + this.newlines(); this.expect('{'); this.separators(); + const statements = []; + while (!this.is('}')) { + const token = this.expect('name', 'assignment, if, or closing brace'); + if (token.value === 'if' && !this.is('=')) { + const condition = this.expression(); + const body = this.block(depth + 1); + statements.push({ kind: 'if', token, condition, body }); + } else { + const name = this.reference(token); + this.expect('='); + statements.push({ kind: 'assign', token, name, expression: this.expression() }); + } + this.end(); + } + this.take(); + return statements; + } + parse() { + const declarations = []; + this.separators(); + while (!this.is('eof')) { + const token = this.expect('name', 'declaration'); + const kind = token.value; + if (kind === 'state') { + const name = this.expect('name', 'state name'); this.expect('='); + declarations.push({ kind, token, name, initial: this.initial() }); + } else if (kind === 'record') { + const name = this.expect('name', 'record type'); this.newlines(); this.expect('{'); this.separators(); + const fields = []; + while (!this.is('}')) { + const field = this.expect('name', 'field name'); this.expect('='); + fields.push({name:field, initial:this.initial()}); this.end(); + } + this.take(); declarations.push({kind,token,name,fields}); + } else if (kind === 'item') { + const name = this.expect('name', 'item name'); this.expect('='); + const type = this.expect('name', 'record type'); this.expect('('); this.newlines(); + const fields = []; + while (!this.is(')')) { + const field = this.expect('name', 'field name'); this.expect('='); + fields.push({name:field, initial:this.initial()}); this.newlines(); + if (!this.is(',')) break; + this.take(); this.newlines(); + } + this.expect(')'); declarations.push({kind,token,name,type,fields}); + } else if (kind === 'rule') { + const name = this.expect('name', 'rule name'); this.expect('('); + const parameter = this.expect('name', 'record parameter'); this.expect(':'); + const type = this.expect('name', 'record type'); this.expect(')'); + declarations.push({kind,token,name,parameter,type,body:this.block()}); + } else if (kind === 'input') { + const name = this.expect('name', 'input name'); this.expect('='); + const key = this.expect('name', 'keyboard code or pointer(view)'); + if (key.value === 'value') { + declarations.push({kind,token,name,valueInput:true}); this.end(); continue; + } + if (key.value === 'pointer') { + if (this.is('{')) { + this.take(); this.separators(); const routes = []; + while (!this.is('}')) { + const target = this.expect('name', 'view ID'); this.expect(':'); + const item = this.expect('name', 'item name'); routes.push({target,item}); this.end(); + } + this.take(); declarations.push({kind,token,name,routes}); this.end(); continue; + } + this.expect('('); + const target = this.expect('name', 'view ID'); this.expect(')'); + declarations.push({ kind, token, name, target }); + this.end(); continue; + } + if (!/^(?:Space|Enter|Arrow(?:Left|Right|Up|Down)|Key[A-Z]|Digit[0-9])$/.test(key.value)) fail(key, 'Unsupported keyboard code'); + declarations.push({ kind, token, name, key }); + } else if (Object.hasOwn(VIEW_FIELDS, kind)) { + const name = this.expect('name', 'view ID'); this.expect('('); + const expressions = []; + for (let i = 0; i < VIEW_FIELDS[kind].length; i++) { + if (i) { this.newlines(); this.expect(','); } + expressions.push(this.expression(0, true)); + } + this.newlines(); + let color, prefix = ''; + if (kind !== 'text') { + this.expect(','); this.newlines(); + const value = this.expect('string', 'literal hex color'); + if (!/^#[0-9a-fA-F]{6}$/.test(value.value)) fail(value, 'Color must use six hexadecimal digits, such as "#356548"'); + color = value.value; + } else if (this.is(',')) { this.take(); this.newlines(); prefix = this.expect('string', 'literal text prefix').value; } + this.newlines(); this.expect(')'); + declarations.push({ kind: 'view', type: kind, token, name, expressions, color, prefix }); + } else if (kind === 'on') { + const event = this.expect('name', 'input name or frame'); + let edge; + if (event.value !== 'frame') { + this.expect('.'); edge = this.expect('name', 'down, up, move or cancel'); + if (!['down', 'up', 'move', 'cancel', 'change'].includes(edge.value)) fail(edge, 'Expected down or up, move, cancel or change'); + } + if (this.is('name', 'use')) { + this.take(); const rule = this.expect('name', 'rule name'); this.expect('('); + const item = this.expect('name', 'item name or input.target'); + const argument = this.reference(item); this.expect(')'); + declarations.push({kind:'handler',token,event,edge,rule,item,argument}); + } else declarations.push({ kind: 'handler', token, event, edge, body: this.block() }); + } else fail(token, `Unknown declaration ${kind}`); + this.end(); + } + return declarations; + } +} + +export function compile(source) { + if (typeof source !== 'string') fail({ line: 1, column: 1 }, 'Source must be a string'); + const sourceBytes = new TextEncoder().encode(source).length; + if (sourceBytes > LIMITS.sourceBytes) fail({ line: 1, column: 1 }, 'Source exceeds 32768 UTF-8 bytes'); + const declarations = new Parser(lex(source)).parse(); + const program = { states: [], inputs: [], instructions: [], handlers: [], views: [], pointerTargets: [], metrics: {} }; + const states = new Map(), inputs = new Map(), views = new Set(), keys = new Set(), targets = new Set(); + const records = new Map(), items = new Map(), rules = new Map(); + const scalarNames = new Set(declarations.filter(d => d.kind === 'state').map(d => d.name.value)); + for (const declaration of declarations) { + if (!['record','rule'].includes(declaration.kind)) continue; + const {name,fields,kind} = declaration, symbols = kind === 'record' ? records : rules; + if (RESERVED.has(name.value)) fail(name, `Reserved name ${name.value}`); + if (symbols.has(name.value)) fail(name, `Duplicate ${kind} ${name.value}`); + if (kind === 'record') { + if (!fields.length || fields.length > 8) fail(name, 'A record needs 1..8 fields'); + const used = new Set(); + for (const field of fields) { + if (used.has(field.name.value)) fail(field.name, `Duplicate field ${field.name.value}`); + if (RESERVED.has(field.name.value)) fail(field.name, `Reserved name ${field.name.value}`); + used.add(field.name.value); + } + } + symbols.set(name.value, declaration); + } + let handlerCount = 0; + for (const declaration of declarations) { + const { kind, name, token } = declaration; + if (name && RESERVED.has(name.value)) fail(name, `Reserved name ${name.value}`); + if (kind === 'record' || kind === 'rule') continue; + if (kind === 'item') { + if (items.has(name.value) || scalarNames.has(name.value)) fail(name, `Duplicate state/item ${name.value}`); + const record = records.get(declaration.type.value); + if (!record) fail(declaration.type, `Unknown record ${declaration.type.value}`); + const overrides = new Map(); + for (const field of declaration.fields) { + if (!record.fields.some(f => f.name.value === field.name.value)) fail(field.name, `Unknown field ${field.name.value}`); + if (overrides.has(field.name.value)) fail(field.name, `Duplicate field ${field.name.value}`); + overrides.set(field.name.value, field.initial); + } + const base = states.size; + for (const field of record.fields) { + if (states.size >= LIMITS.states) fail(name, 'At most 32 states are allowed, including item fields'); + const qualified = `${name.value}.${field.name.value}`, index = states.size; + states.set(qualified,index); + program.states.push({name:qualified,initial:overrides.has(field.name.value) ? overrides.get(field.name.value) : field.initial,line:token.line}); + } + items.set(name.value,{type:declaration.type.value,base,count:record.fields.length}); + } else if (kind === 'state' || kind === 'input') { + const symbols = kind === 'state' ? states : inputs; + if (symbols.has(name.value)) fail(name, `Duplicate ${kind} ${name.value}`); + if (symbols.size >= LIMITS[`${kind}s`]) fail(name, `At most ${LIMITS[`${kind}s`]} ${kind}s are allowed`); + symbols.set(name.value, symbols.size); + if (kind === 'state') program.states.push({ name: name.value, initial: declaration.initial, line: token.line }); + else if (declaration.valueInput) { + program.inputs.push({name:name.value,kind:'value',line:token.line}); + } else if (declaration.routes) { + if (!declaration.routes.length) fail(name, 'A pointer group needs at least one target'); + program.inputs.push({name:name.value, routes:[], line:token.line}); + } else if (declaration.target) { + const target = declaration.target; + if (targets.has(target.value)) fail(target, `Duplicate pointer target ${target.value}`); + targets.add(target.value); + program.inputs.push({ name: name.value, target: target.value, line: token.line }); + } else { + if (keys.has(declaration.key.value)) fail(declaration.key, `Duplicate keyboard code ${declaration.key.value}`); + keys.add(declaration.key.value); + program.inputs.push({ name: name.value, key: declaration.key.value, line: token.line }); + } + } else if (kind === 'view') { + if (views.has(name.value)) fail(name, `Duplicate view ID ${name.value}`); + if (views.size >= LIMITS.views) fail(name, 'At most 24 views are allowed'); + views.add(name.value); + } else if (++handlerCount > LIMITS.handlers) fail(token, 'At most 64 handlers are allowed'); + } + for (const declaration of declarations) { + if (declaration.target && !views.has(declaration.target.value)) + fail(declaration.target, `Unknown pointer target ${declaration.target.value}`); + } + const routedItems = new Set(); + for (const declaration of declarations) { + if (!declaration.routes) continue; + const input = inputs.get(declaration.name.value), resolved = program.inputs[input]; + let type; + for (const {target,item:token} of declaration.routes) { + if (!views.has(target.value)) fail(target, `Unknown pointer target ${target.value}`); + if (targets.has(target.value)) fail(target, `Duplicate pointer target ${target.value}`); + const item = items.get(token.value); + if (!item) fail(token, `Unknown item ${token.value}`); + if (type && type !== item.type) fail(token, 'Pointer group targets must share one record type'); + if (routedItems.has(token.value)) fail(token, `Item ${token.value} already has a pointer route`); + targets.add(target.value); routedItems.add(token.value); type = item.type; + const route = program.pointerTargets.length; + if (route >= 24) fail(target, 'At most 24 pointer targets are allowed'); + program.pointerTargets.push({input,base:item.base,count:item.count}); + resolved.routes.push({target:target.value, item:token.value, route}); + } + resolved.type = type; + } + for (const rule of rules.values()) { + if (!records.has(rule.type.value)) fail(rule.type, `Unknown record ${rule.type.value}`); + if (RESERVED.has(rule.parameter.value)) fail(rule.parameter, `Reserved name ${rule.parameter.value}`); + } + let pointHandler = false, valueHandler = false; + let parameters = null; + let parameterPrefix = null; + let stack = 0, maxStack = 0; + const emit = (op, token, arg = 0, value = 0, delta = 0) => { + if (program.instructions.length >= LIMITS.instructions) fail(token, 'At most 512 instructions are allowed'); + stack += delta; + if (stack < 0 || stack > LIMITS.stack) fail(token, 'Expression stack exceeds 64'); + maxStack = Math.max(maxStack, stack); + program.instructions.push({ op, arg, value, line: token.line }); + }; + const expression = (node, binding = false) => { + const { token } = node; + if (node.kind === 'constant') emit(0, token, 0, node.value, 1); + else if (node.kind === 'reference') { + if (node.name === 'event_value') { + if (binding || !valueHandler) fail(token, 'event_value is only available in value change handlers'); + emit(26, token, 0, 0, 1); + } else if (node.name === 'event_x' || node.name === 'event_y') { + if (binding || !pointHandler) fail(token, 'event_x and event_y are only available in pointer handlers'); + emit(node.name === 'event_x' ? 22 : 23, token, 0, 0, 1); + } else if (node.name === 'dt') { + if (binding) fail(token, 'dt is only available in handlers'); + emit(2, token, 0, 0, 1); + } else { + if (parameterPrefix && node.name.startsWith(parameterPrefix) && !parameters.has(node.name)) + fail(token, `Unknown parameter field ${node.name}`); + if (parameters?.has(node.name)) { + emit(24, token, parameters.get(node.name), 0, 1); + return; + } + if (!states.has(node.name)) fail(token, `Unknown state ${node.name}`); + emit(1, token, states.get(node.name), 0, 1); + } + } else if (node.kind === 'unary') { expression(node.child, binding); emit(node.op, token); } + else if (node.kind === 'binary') { + expression(node.left, binding); expression(node.right, binding); emit(node.op, token, 0, 0, -1); + } else { + for (const arg of node.args) expression(arg, binding); + emit(node.op, token, 0, 0, 1 - node.args.length); + } + }; + const statements = body => { + for (const statement of body) { + if (statement.kind === 'assign') { + if (parameterPrefix && statement.name.startsWith(parameterPrefix) && !parameters.has(statement.name)) + fail(statement.token, `Unknown parameter field ${statement.name}`); + const parameter = parameters?.get(statement.name); + if (parameter === undefined && !states.has(statement.name)) fail(statement.token, `Unknown state ${statement.name}`); + expression(statement.expression); + emit(parameter === undefined ? 20 : 25, statement.token, parameter === undefined ? states.get(statement.name) : parameter, 0, -1); + } else { + expression(statement.condition); + const branch = program.instructions.length; + emit(21, statement.token, 0, 0, -1); + statements(statement.body); + program.instructions[branch].arg = program.instructions.length; + } + } + }; + const bodies = new Map(); + function ruleBody(rule) { + if (bodies.has(rule.name.value)) return bodies.get(rule.name.value); + parameterPrefix = `${rule.parameter.value}.`; + parameters = new Map(records.get(rule.type.value).fields.map((field,index) => [`${rule.parameter.value}.${field.name.value}`,index])); + // A shared body may read coordinates; every binding is checked below. + pointHandler = true; valueHandler = true; stack = 0; + const start = program.instructions.length; + statements(rule.body); + const length = program.instructions.length - start; + const point = program.instructions.slice(start).some(op => op.op === 22 || op.op === 23); + parameters = null; parameterPrefix = null; + const value = program.instructions.slice(start).some(op => op.op === 26); + const body = {start,length,point,value}; bodies.set(rule.name.value,body); return body; + } + for (const declaration of declarations) { + if (declaration.kind === 'handler') { + let event = 0; + pointHandler = false; valueHandler = false; + if (declaration.event.value !== 'frame') { + if (!inputs.has(declaration.event.value)) fail(declaration.event, `Unknown input ${declaration.event.value}`); + const index = inputs.get(declaration.event.value), edge = declaration.edge.value; + pointHandler = Boolean(program.inputs[index].target || program.inputs[index].routes); + valueHandler = program.inputs[index].kind === 'value'; + if (valueHandler !== (edge === 'change')) fail(declaration.edge, 'Value inputs require change; change requires a value input'); + if (!pointHandler && (edge === 'move' || edge === 'cancel')) fail(declaration.edge, 'move and cancel require a pointer input'); + event = edge === 'change' ? 33 + index : edge === 'move' ? 17 + index : edge === 'cancel' ? 25 + index : 1 + index * 2 + (edge === 'up' ? 1 : 0); + } + if (declaration.rule) { + const rule = rules.get(declaration.rule.value); + const routed = declaration.argument.includes('.'); + const input = program.inputs[inputs.get(declaration.event.value)]; + if (routed && (!input?.routes || declaration.argument !== `${declaration.event.value}.target`)) + fail(declaration.item, 'Use the current pointer group target'); + const item = routed ? {type:input.type, count:records.get(input.type).fields.length} : items.get(declaration.item.value); + if (!rule) fail(declaration.rule, `Unknown rule ${declaration.rule.value}`); + if (!item) fail(declaration.item, `Unknown item ${declaration.item.value}`); + if (item.type !== rule.type.value) fail(declaration.item, `Rule ${rule.name.value} requires ${rule.type.value}`); + const pointer = pointHandler, value = valueHandler; + const body = ruleBody(rule); + if (body.value && !value) fail(declaration.rule, 'Value-reading rules require a value input'); + if (body.point && !pointer) fail(declaration.rule, 'Coordinate-reading rules require a pointer input'); + program.handlers.push({event,start:body.start,length:body.length,line:declaration.token.line,parameterBase:routed ? 255 : item.base,parameterCount:item.count}); + } else { + const start = program.instructions.length; + stack = 0; statements(declaration.body); + program.handlers.push({ event, start, length: program.instructions.length - start, line: declaration.token.line }); + } + } else if (declaration.kind === 'view') { + const view = { id: declaration.name.value, type: declaration.type, line: declaration.token.line, bindings: {} }; + if (view.type === 'text') view.prefix = declaration.prefix; + else view.color = declaration.color; + declaration.expressions.forEach((node, index) => { + const start = program.instructions.length; + stack = 0; expression(node, true); + view.bindings[VIEW_FIELDS[view.type][index]] = { start, length: program.instructions.length - start }; + }); + program.views.push(view); + } + } + // Unused definitions still receive name/type checking, but reserve no VM body. + for (const rule of rules.values()) { + if (bodies.has(rule.name.value)) continue; + const instructions = program.instructions, previousMax = maxStack; + program.instructions = []; + ruleBody(rule); + program.instructions = instructions; maxStack = previousMax; + } + program.metrics = { sourceBytes, sourceLines: source === '' ? 0 : source.split('\n').length, + states: states.size, inputs: inputs.size, views: views.size, + handlers: program.handlers.length, instructions: program.instructions.length, maxStack }; + return program; +} + +export { LIMITS }; diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/core.wasm b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/core.wasm Binary files differnew file mode 100644 index 0000000..79c50a8 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/core.wasm diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/project.mjs b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/project.mjs new file mode 100644 index 0000000..861e0fc --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/project.mjs @@ -0,0 +1,47 @@ +import { compile } from './compiler.mjs'; + +// Two escaped source strings plus up to 32 generated item.field names. A long +// schema field can appear in every item without repeating in the source text. +export const MAX_PROJECT_BYTES = (2 * 6 + 32) * 32768 + 8192; +const encoder = new TextEncoder(); +function sourceText(value, label) { + if (typeof value !== 'string' || encoder.encode(value).length > 32768) + throw new Error(`${label}: UTF-8 32 KiB 이하의 코드가 필요합니다.`); + return value; +} + +// No runtime mutation. Validate every saved value before opening core staging. +export function validateDocument(document) { + if (!document || typeof document !== 'object' || Array.isArray(document) || + document.format !== 'pure-workbench' || document.version !== 2) + throw new Error('배치 파일 형식이 맞지 않습니다. pure-workbench version 2가 필요합니다.'); + const source = sourceText(document.source, '적용된 코드'); + const draft = sourceText(Object.hasOwn(document, 'draft') ? document.draft : source, '편집 중인 코드'); + const program = compile(source); + if (!Array.isArray(document.states) || document.states.length !== program.states.length) + throw new Error('저장된 상태 목록이 적용된 코드와 일치하지 않습니다.'); + const values = new Map(); + for (const entry of document.states) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.name !== 'string' || values.has(entry.name)) + throw new Error('상태 이름이 잘못됐거나 중복됐습니다.'); + const value = entry.value === '-0' ? -0 : entry.value; + if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error('상태 값은 유한한 숫자여야 합니다.'); + values.set(entry.name, value); + } + for (const state of program.states) + if (!values.has(state.name)) throw new Error(`저장된 상태가 없습니다: ${state.name}`); + return { source, draft, program, values: program.states.map(state => values.get(state.name)) }; +} + +export function parseProject(text) { + if (typeof text !== 'string' || encoder.encode(text).length > MAX_PROJECT_BYTES) + throw new Error('작업 파일이 너무 큽니다.'); + let project; + try { project = JSON.parse(text); } catch { throw new Error('올바른 JSON 파일이 아닙니다.'); } + if (!project || typeof project !== 'object' || Array.isArray(project) || project.format !== 'pure-workbench') + throw new Error('Pure 작업물 형식이 아닙니다.'); + if (project.version === 1) sourceText(project.source, '코드'); + else if (project.version === 2) validateDocument(project); + else throw new Error('지원하지 않는 작업 파일 버전입니다.'); + return project; +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/runtime.mjs b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/runtime.mjs new file mode 100644 index 0000000..d8d6618 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/sources/prototypes/workbench/web/runtime.mjs @@ -0,0 +1,350 @@ +import { compile } from './compiler.mjs'; +import { validateDocument } from './project.mjs'; + +const messages = [ + '', '실행기의 용량 한도를 넘었습니다.', '실행 정의가 유효하지 않습니다.', + '계산 스택의 범위를 벗어났습니다.', '0으로 나눌 수 없습니다.', + '계산 결과가 유한한 수의 범위를 벗어났습니다.', '조건 분기 범위가 유효하지 않습니다.', + '존재하지 않는 상태를 참조했습니다.', '시간 간격이 유효하지 않습니다.', + '정의되지 않은 입력 사건입니다.', +]; +const sourceLimit = 32768; + +function problem(message, line = null, column = null) { + return Object.assign(new Error(message), { line, column }); +} + +function freezeProgram(value) { + if (value && typeof value === 'object') { + for (const child of Object.values(value)) freezeProgram(child); + Object.freeze(value); + } + return value; +} + +export async function createRuntime(wasmBytes, evaluationOptions = {}) { + if (wasmBytes === undefined) { + const response = await fetch(new URL('./core.wasm', import.meta.url), { cache: 'no-store' }); + if (!response.ok) throw new Error(`실행기를 불러오지 못했습니다 (HTTP ${response.status}).`); + wasmBytes = await response.arrayBuffer(); + } + const module = await WebAssembly.compile(wasmBytes); + if (WebAssembly.Module.imports(module).length) throw new Error('이 실행기는 호스트 함수 없이 동작해야 합니다.'); + const { exports: core } = await WebAssembly.instantiate(module, {}); + if (typeof core.eval_bounded !== 'function' || typeof core.abort_program !== 'function' || typeof core.dispatch_point !== 'function' || typeof core.put_restored_value !== 'function' || typeof core.commit_restored_program !== 'function' || typeof core.bind_handler !== 'function' || typeof core.put_pointer_target !== 'function' || typeof core.bind_target_handler !== 'function' || typeof core.dispatch_target !== 'function' || typeof core.dispatch_value !== 'function') + throw new Error('실행기 버전이 맞지 않습니다. 새로고침하거나 core.wasm을 다시 빌드하세요.'); + core.reset_all(); + let program = null; + let appliedSource = ''; + let lastEvent = null; + // Keep a failed event visible through its release/focus-loss cleanup. + // The active program can clear this diagnostic by apply or explicit reset. + let lastFailure = null; + const held = new Map(); + const points = new Map(); + let presenting = false; + let bindings = []; + let pointRoutes = new Map(); + // One bounded publication buffer, never exposed to callers. No state mirror. + let displayValues = null, displayValid = null; + function available() { + if (presenting) throw new Error('출력 전달 중에는 실행기를 다시 호출할 수 없습니다.'); + } + let evaluationPolicy = { cacheEntries: 0, historyEntries: 8, workLimit: 2048 }; + + function checked(code, line = null) { + if (code !== 0) throw problem(messages[code] || `실행기 오류 ${code}`, line); + } + function inputIndex(name) { + const index = program?.inputs.findIndex((input) => input.name === name) ?? -1; + if (index < 0) throw new Error(`정의되지 않은 입력: ${name}`); + return index; + } + function dispatch(event, name, phase, source, point = null, value = undefined) { + const status = value !== undefined ? core.dispatch_value(event, value) : point ? (point.route === undefined ? core.dispatch_point(event, point.x, point.y) + : core.dispatch_target(event, point.route, point.x, point.y)) : core.dispatch(event, event === 0 ? 1 / 60 : 0); + const pc = core.info(2) >>> 0; + lastEvent = { name, phase, source, status, + line: program?.instructions[pc]?.line ?? null, ops: core.info(3) >>> 0 }; + if (point) { lastEvent.x = point.x; lastEvent.y = point.y; if (point.target) lastEvent.target = point.target; } + if (value !== undefined) lastEvent.value = value; + if (status) lastFailure = { ...lastEvent }; + return status; + } + + function evaluateDisplay() { + core.begin_evaluation(evaluationPolicy.workLimit); + if (!bindings.length) return null; + displayValues ??= new Float64Array(96); + displayValid ??= new Uint8Array(24); + let error = null, slot = 0; + for (let i = 0; i < bindings.length; i++) { + const view = program.views[i]; + displayValid[i] = 1; + for (const [name, binding] of bindings[i]) { + const value = core.eval_bounded(binding.start, binding.length); + if (core.evaluation_info(0) === 10) { + displayValid.fill(0); + return { code: 101, line: view.line, + message: '표시 계산의 작업량 한도를 넘었습니다. 한도를 늘리거나 표시식을 줄이세요.' }; + } + if (!Number.isFinite(value) || ((name === 'width' || name === 'height' || name === 'radius') && value < 0)) { + error ??= { line: view.line, message: `${view.id}.${name}: 표시할 수 없는 값입니다. 0 나눗셈이나 음수 크기를 확인하세요.` }; + displayValid[i] = 0; + } + displayValues[slot++] = value; + } + } + return error; + } + + function status(viewError) { + const executionError = lastFailure?.status || 0; + return { + tick: core.info(0) >>> 0, revision: core.info(4) >>> 0, + displayError: viewError ? { code: viewError.code || 100, ...viewError } : null, + lastError: executionError || (viewError ? viewError.code || 100 : 0), + errorMessage: executionError ? messages[executionError] || `실행 오류 ${executionError}` : viewError?.message || '', + errorLine: executionError ? lastFailure.line : viewError?.line ?? null, + lastOps: core.info(3) >>> 0, successfulEvents: core.info(9) >>> 0, + held: [...held.keys()], lastEvent: lastEvent ? { ...lastEvent } : null, + lastFailure: lastFailure ? { ...lastFailure } : null, + memory: { + linearBytes: core.memory.buffer.byteLength, + reservedEngineBytes: core.layout(0), programReservedBytes: core.layout(1), + instructionBytes: core.layout(2), handlerBytes: core.layout(3), + activeIRBytes: core.layout(4), activeStateBytes: core.layout(5), + stagingReservedBytes: core.layout(6), evalStackBytes: core.layout(7), + pointerTargetBytes: core.layout(8), pointerTargets: core.layout(9), + }, + }; + } + + function load(next, source, restored = null) { + const oldIds = new Map(restored ? [] : program?.states.map((state, index) => [state.name, index]) ?? []); + checked(core.begin_program(next.states.length, next.inputs.length)); + try { + for (let index = 0; index < next.states.length; index++) { + const state = next.states[index]; + checked(core.put_state(index, state.initial, oldIds.get(state.name) ?? -1), state.line); + if (restored) checked(core.put_restored_value(index, restored[index]), state.line); + } + for (const op of next.instructions) checked(core.put_instruction(op.op, op.arg, op.value), op.line); + for (const target of next.pointerTargets) checked(core.put_pointer_target(target.input, target.base, target.count)); + for (const [index, handler] of next.handlers.entries()) { + checked(core.put_handler(handler.event, handler.start, handler.length), handler.line); + if (handler.parameterCount) + checked(handler.parameterBase === 255 ? core.bind_target_handler(index, handler.parameterCount) + : core.bind_handler(index, handler.parameterBase, handler.parameterCount), handler.line); + } + checked(restored ? core.commit_restored_program() : core.commit_program()); + } catch (error) { + core.abort_program(); + throw error; + } + pointRoutes = new Map(next.inputs.flatMap(input => (input.routes ?? []).map(route => [route.target, route]))); + program = next; + bindings = next.views.map(view => Object.entries(view.bindings)); + appliedSource = source; + lastEvent = null; + lastFailure = null; + } + + const api = { + get currentProgram() { return program; }, + configureEvaluation({ cacheEntries = evaluationPolicy.cacheEntries, + historyEntries = evaluationPolicy.historyEntries, workLimit = evaluationPolicy.workLimit } = {}) { + available(); + for (const [name, value, maximum] of [['cacheEntries', cacheEntries, 32], + ['historyEntries', historyEntries, 8], ['workLimit', workLimit, 0xffffffff]]) { + if (!Number.isInteger(value) || value < 0 || value > maximum) + throw new Error(`${name}: 0~${maximum} 사이의 정수여야 합니다.`); + } + checked(core.configure_evaluation(cacheEntries, historyEntries)); + evaluationPolicy = { cacheEntries, historyEntries, workLimit }; + return { ...evaluationPolicy }; + }, + forgetEvaluation(part = 'all') { + available(); + const flags = part === 'all' ? 3 : part === 'values' ? 1 : part === 'history' ? 2 : undefined; + if (flags === undefined) throw new Error('삭제할 기록은 values, history, all 중 하나여야 합니다.'); + core.forget_evaluation(flags); + }, + evaluationStats() { + return { ...evaluationPolicy, work: core.evaluation_info(1), + instructions: core.evaluation_info(2), cacheHits: core.evaluation_info(3), + reservedBytes: core.evaluation_info(6), dedicatedBytes: core.evaluation_info(7), + sharedBytes: core.evaluation_info(8), reuseAvailable: Boolean(core.evaluation_info(9)), + workspaceTagBytes: core.evaluation_info(10) }; + }, + apply(source) { + available(); + if (typeof source !== 'string' || new TextEncoder().encode(source).length > sourceLimit) { + throw problem('정의는 UTF-8 기준 32,768바이트 이하의 텍스트여야 합니다.'); + } + if (held.size) throw problem('누르고 있는 입력을 놓은 뒤 적용하세요. 현재 작업은 그대로 유지합니다.'); + const next = freezeProgram(compile(source)); + load(next, source); + return next; + }, + restoreDocument(document) { + available(); + if (held.size) throw problem('누르고 있는 입력을 놓은 뒤 복원하세요.'); + const next = validateDocument(document); + load(freezeProgram(next.program), next.source, next.values); + return { source: next.source, draft: next.draft }; + }, + exportDocument(draft = appliedSource) { + available(); + if (!program) throw problem('먼저 코드를 적용하세요.'); + if (held.size) throw problem('조작을 끝낸 뒤 배치를 저장하세요.'); + if (lastFailure) throw problem('실행 오류를 해결한 뒤 배치를 저장하세요.'); + if (typeof draft !== 'string' || new TextEncoder().encode(draft).length > sourceLimit) + throw problem('편집 중인 코드는 UTF-8 32 KiB 이하여야 합니다.'); + return { format: 'pure-workbench', version: 2, source: appliedSource, draft, + states: program.states.map((state, i) => { + const value = core.get_state(i); + return { name: state.name, value: Object.is(value, -0) ? '-0' : value }; + }) }; + }, + reset() { + available(); + held.clear(); + points.clear(); + core.reset_state(); + lastEvent = null; + lastFailure = null; + }, + frame(count = 1) { + available(); + if (!Number.isInteger(count) || count < 0 || count > 600) throw new Error('프레임 수는 0~600 사이의 정수여야 합니다.'); + if (!program) return 0; + for (let i = 0; i < count; i++) { + const code = dispatch(0, 'frame', 'frame', 'clock'); + if (code !== 0) return code; + } + return 0; + }, + down(name, source = 'external') { + available(); + const index = inputIndex(name); + if (program.inputs[index].kind === 'value') throw new Error('숫자 입력은 change(name, value)를 사용하세요.'); + if (program.inputs[index].target || program.inputs[index].routes) throw new Error('좌표 입력은 point(name, phase, x, y)를 사용하세요.'); + const sources = held.get(name) || new Set(); + if (sources.has(source)) return 0; + const wasDown = sources.size > 0; + sources.add(source); + held.set(name, sources); + return wasDown ? 0 : dispatch(1 + index * 2, name, 'down', source); + }, + up(name, source = 'external') { + available(); + const index = inputIndex(name); + if (program.inputs[index].kind === 'value') throw new Error('숫자 입력은 change(name, value)를 사용하세요.'); + if (program.inputs[index].target || program.inputs[index].routes) throw new Error('좌표 입력은 point(name, phase, x, y)를 사용하세요.'); + const sources = held.get(name); + if (!sources?.delete(source)) return 0; + if (sources.size) return 0; + held.delete(name); + return dispatch(2 + index * 2, name, 'up', source); + }, + change(name, value, source = 'external') { + available(); + const index = inputIndex(name); + if (program.inputs[index].kind !== 'value') throw new Error('숫자 값에 연결된 입력이 아닙니다.'); + if (!Number.isFinite(value)) throw new Error('입력 값은 유한한 숫자여야 합니다.'); + return dispatch(33 + index, name, 'change', source, null, value); + }, + point(name, phase, x, y, source = 'external', target = undefined) { + available(); + const index = inputIndex(name); + const input = program.inputs[index]; + if (!input.target && !input.routes) throw new Error('포인터에 연결된 입력이 아닙니다.'); + const route = input.routes ? pointRoutes.get(target) : undefined; + if (input.routes && (!route || program.pointerTargets[route.route].input !== index)) throw new Error('포인터 그룹에 연결된 도형 이름이 필요합니다.'); + if (!input.routes && target !== undefined) throw new Error('단일 포인터 입력에는 대상 인자를 지정하지 않습니다.'); + const owner = route ? `${name}:${target}` : name; + if (!['down', 'move', 'up', 'cancel'].includes(phase)) throw new Error('포인터 단계는 down, move, up, cancel 중 하나여야 합니다.'); + if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error('포인터 좌표는 유한한 숫자여야 합니다.'); + if (typeof source !== 'string' || source.length === 0) throw new Error('포인터 출처 이름이 필요합니다.'); + let point = points.get(owner); + if (phase === 'down') { + if (point?.source === source) return 0; + if (point) throw new Error('이 입력은 다른 포인터가 사용 중입니다.'); + point = {name, source, x, y, target, route:route?.route}; + points.set(owner, point); + held.set(owner, new Set([source])); + } else { + if (!point || point.source !== source) return 0; + point.x = x; point.y = y; + if (phase === 'up' || phase === 'cancel') { points.delete(owner); held.delete(owner); } + } + const event = phase === 'move' ? 17 + index : phase === 'cancel' ? 25 + index : 1 + index * 2 + (phase === 'up' ? 1 : 0); + return dispatch(event, name, phase, source, point); + }, + releaseAll() { + available(); + let firstError = 0; + for (const point of [...points.values()]) { + const status = api.point(point.name, 'cancel', point.x, point.y, point.source, point.target); + if (!firstError) firstError = status; + } + for (const [name, sources] of [...held]) { + for (const source of [...sources]) { + const status = api.up(name, source); + if (!firstError) firstError = status; + } + } + return firstError; + }, + exportProject() { + return { format: 'pure-workbench', version: 1, source: appliedSource }; + }, + // Synchronous trusted output adapter. Callbacks run only after the whole + // display has been evaluated. Runtime mutation/reentrant reads are refused. + // Exceptions from an adapter propagate; the next call republishes all values. + present(target) { + available(); + presenting = true; + try { + const { begin, state, view, value, end } = target; + for (const callback of [begin, state, view, value, end]) + if (typeof callback !== 'function') throw new TypeError('출력 어댑터 함수가 필요합니다.'); + const error = evaluateDisplay(); + begin.call(target, program); + for (let i = 0; i < (program?.states.length ?? 0); i++) state.call(target, i, core.get_state(i)); + let slot = 0, visible = 0; + for (let i = 0; i < bindings.length; i++) { + const valid = Boolean(displayValid[i]); + view.call(target, i, valid); + for (const [name] of bindings[i]) { + if (valid) value.call(target, i, name, displayValues[slot]); + slot++; + } + if (valid) visible++; + } + end.call(target, visible); + return status(error); + } finally { presenting = false; } + }, + snapshot() { + available(); + const error = evaluateDisplay(); + const states = Object.fromEntries((program?.states ?? []).map((state, index) => [state.name, core.get_state(index)])); + const views = []; + let slot = 0; + for (let i = 0; i < bindings.length; i++) { + const view = program.views[i]; + const rendered = displayValid[i] ? { id: view.id, type: view.type, color: view.color, prefix: view.prefix } : null; + for (const [name] of bindings[i]) { + if (rendered) rendered[name] = displayValues[slot]; + slot++; + } + if (rendered) views.push(rendered); + } + return { states, views, ...status(error) }; + }, + }; + api.configureEvaluation(evaluationOptions); + return Object.freeze(api); +} diff --git a/prototypes/connections/results/2026-09-23-document-edits/verification.json b/prototypes/connections/results/2026-09-23-document-edits/verification.json new file mode 100644 index 0000000..598aed9 --- /dev/null +++ b/prototypes/connections/results/2026-09-23-document-edits/verification.json @@ -0,0 +1,32 @@ +{ + "sources_verified_and_frozen": 37, + "native_and_sanitized_source_hashes_match": true, + "recorded_binaries_verified": 8, + "integration_scenarios_per_build": 36, + "server_runs_per_build": 19, + "all_server_runs_drained": true, + "compiler": "cc (Debian 14.2.0-19) 14.2.0", + "core_sections": { + "text": 5932, + "data": 0, + "bss": 0 + }, + "structure_sizes": { + "domain_bytes": 200, + "work_slot_bytes": 224, + "ticket_bytes": 24, + "prepared_program_bytes": 680, + "pointer_bytes": 8 + }, + "core_external_symbols": [ + "memcpy", + "strlen" + ], + "server_structure_bytes": [41528], + "limitations": [ + "synchronous full-file rewrite with 4096-byte host scratch", + "11-byte credit test does not measure total memory", + "limited storage authoring profile, no native GUI or secure peer RPC", + "GNU footprint tooling validated on Linux AArch64; macOS unverified" + ] +} |
